利用至多一个奇数位置的承诺,把破绽定位转化为前缀防具总数奇偶性的 0→1 阶跃,在阶跃上二分出翻转点,再单点计数组数得到件数。
OJ: roj
题目 ID: 3025
难度:提高
标签:二分前缀和奇偶性python
创建: 2026-10-01 10:40
更新: 2026-10-07 13:50
形式化题目
把防线看成一条整数坐标轴。给定
每个坐标上放 1 件防具。位置
已知整条防线上至多一个位置的防具数为奇数(0 也算偶数)。求这个位置 There's no weakness.。
例如样例 3 的四组防具为 4 3。
正解
思路
朴素做法:枚举防线上的每个坐标
换个问法:我们并不需要知道每个位置的件数,只需要找到唯一的奇数位置。题目"至多一个奇数位置"的承诺,恰好把逐位置的信息压缩成一个前缀奇偶性。记
除破绽
也就是说,判定值 F(x) & 1 是一个在
判定的
F(x) & 1 = 各组贡献求和后取最低位,全程只做整数四则运算,不枚举任何位置或防具。
二分区间:取 lo = min S_i - 1(左侧没有任何防具,判定必为 0)、hi = max E_i。先算 F(hi) & 1:若为 0,说明连总数都是偶数,直接输出 There's no weakness.;若为 1,则在 mid 判定为 1 就收右边界,否则收左边界,while hi - lo > 1 结束时 hi 即为第一个翻转点
以样例 3 展示逐位置防具数与前缀奇偶性的变化:
| 位置 |
覆盖 |
该处件数 | ||
|---|---|---|---|---|
| 0 | — | 0 | 0 | 0 |
| 1 | A, C | 2 | 2 | 0 |
| 2 | A, C | 2 | 4 | 0 |
| 3 | A, C | 2 | 6 | 0 |
| 4 | A, B, C | 3 | 9 | 1 |
| 5 | A, C | 2 | 11 | 1 |
| 6 | A, D | 2 | 13 | 1 |
| 7 ~ 10 | A, D | 2 | 15 ~ 21 | 1 |
表中 A~D 依次是四组防具。奇偶性只在 lo=0, hi=10 → mid=5 判定 1 → hi=5 → mid=2 判定 0 → lo=2 → mid=3 判定 0 → lo=3 → mid=4 判定 1 → hi=4,此时 hi - lo = 1,得到
最后求
两处实现细节:题面只保证 lo = min S_i - 1 允许取到
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 10:44
# update_at: 2026-10-01 10:44
import sys
def prefix_parity(groups: list[tuple[int, int, int]], x: int) -> int:
"""位置 x 及其左侧防具总数的奇偶性:1 表示唯一破绽落在 x 或其左侧。"""
return sum(
(min(e, x) - s) // d + 1 if x >= s else 0 # 该组在 [S, x] 区间内的件数
for s, e, d in groups
) & 1
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
T = next(data)
for _ in range(T):
n = next(data)
groups = [(next(data), next(data), next(data)) for _ in range(n)]
groups = [(s, e if d else s, d or 1) for s, e, d in groups] # D=0 时退化为单点 S
lo = min(s for s, _, _ in groups) - 1 # lo 左侧没有任何防具,奇偶性必为 0
hi = max(e for _, e, _ in groups)
if prefix_parity(groups, hi) == 0:
out.append("There's no weakness.") # 整条防线累计仍是偶数:没有破绽
continue
# 奇偶性在破绽 P 处由 0 翻转为 1,且之后恒为 1:二分找第一个翻转位置
while hi - lo > 1:
mid = (lo + hi) // 2
if prefix_parity(groups, mid):
hi = mid
else:
lo = mid
p = hi
# 每组等差数列至多在 p 处放一件,直接数出落在 p 上的组数即为该处件数
count = sum(1 for s, e, d in groups if s <= p <= e and (p - s) % d == 0)
out.append(f"{p} {count}")
print("\n".join(out))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 14:42
* update_at: 2026-10-06 14:42
*/
#include <cstdio>
typedef long long ll;
const int MAXN = 200005;
ll S[MAXN]; // 第 i 组防具的起点
ll E[MAXN]; // 第 i 组防具的终点
ll D[MAXN]; // 第 i 组防具的步长,D=0 时归一为 1
int n; // 当前测试数据的防具组数
// 位置 x 及左侧防具总数的奇偶性:1 表示破绽在 x 或其左侧
ll prefix_parity(ll x) {
ll total = 0;
for (int i = 1; i <= n; i++) {
if (x < S[i]) {
continue;
}
// 第 i 组在 [S_i, min(E_i, x)] 内的等差数列项数
ll right = E[i] < x ? E[i] : x;
total += (right - S[i]) / D[i] + 1;
}
return total & 1;
}
int main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
ll s, e, d;
scanf("%lld %lld %lld", &s, &e, &d);
if (d == 0) {
e = s; // 步长为 0 时等差数列退化成单点 S
d = 1;
}
S[i] = s;
E[i] = e;
D[i] = d;
}
ll lo = S[1] - 1; // lo 左侧没有任何防具,前缀奇偶性必为 0
ll hi = E[1];
for (int i = 2; i <= n; i++) {
if (S[i] - 1 < lo) {
lo = S[i] - 1;
}
if (E[i] > hi) {
hi = E[i];
}
}
if (prefix_parity(hi) == 0) {
printf("There's no weakness.\n"); // 总防具数为偶数,无破绽
continue;
}
// 奇偶性在破绽处由 0 翻转为 1,二分找到第一个翻转位置
while (hi - lo > 1) {
ll mid = (lo + hi) / 2;
if (prefix_parity(mid)) {
hi = mid;
} else {
lo = mid;
}
}
ll p = hi;
// 每组等差数列至多在 p 处放一件,数出覆盖 p 的组数即为该处件数
ll cnt = 0;
for (int i = 1; i <= n; i++) {
if (S[i] <= p && p <= E[i] && (p - S[i]) % D[i] == 0) {
cnt++;
}
}
printf("%lld %lld\n", p, cnt);
}
return 0;
}复杂度
- 时间复杂度:每次判定
,二分区间长度不超过 ,至多 32 轮,故每组数据 , ; 组总计 ,最坏约 次整数运算,与标准 C++ 解法同阶。 - 空间复杂度:
存放 组参数,输入整体一次读入;实测最大峰值 85 MB,低于 限制。
总结
防线的关键是题面里那句"至多只有一个位置有奇数个防具"。它把"每个位置各是多少"的逐点统计,压缩成前缀防具总数奇偶性的一次 0→1 翻转:判定 F(x) & 1 只需对每组做一次等差数列项数的整数运算,单调性让翻转点可以直接二分出来,定位后再对单点做一次组数统计得到件数。整套做法