1. 比赛概览与做题规划
先说结论:Codeforces Round 1080(Div. 3)这套题,是我最近复盘时反复拿出来研究的一套。表面上看它只是 Div. 3 的难度,对很多老选手来说不算硬核,但整套题从 A 到 F 的梯度设计非常典型,特别是 D 题“Xor of 3”和 F 题“Polycarp and Snakes”,一个考异或性质和奇偶性思维,一个考模拟与分类讨论,恰恰是之后打 Div. 2、甚至 Div. 1 时经常要用的基础能力。
我的建议是:如果你刚开始打 Codeforces,或者正在冲击 Div. 3 全过,不要只把这场当成“水题场”刷一遍就扔。这套题里每一道都值得你把思路写成文字,尤其是你卡过、wa 过、甚至看了题解才明白的题,才是真正涨分的部分。我这次复盘就把我做题时的完整思路、代码实现和踩坑记录放在一起,方便你照着走一遍。
1.1 这套题的难度分布
从实际参赛体感来看,A、B 两道属于“读题就能做”的签到题,C 题是经典容斥加坐标处理,D 题开始需要一点思维量,E 题不算难但很考验细心,F 题则是一场模拟题,代码量小,但很容易在细节上翻车。
我个人把这场难度曲线画成“平缓爬坡、最后一个小陡坡”。前面 A-C 基本是在帮选手热手,真正拉开差距的是 D 题和 F 题。D 题如果你之前没接触过 XOR 操作的奇偶性问题,很容易卡在“我该构造什么操作序列”上;F 题则是一道纯纯的“规则题”,题目描述不长,但你要把“合法蛇”的定义吃透,否则很容易用错误的判定条件写完整道题。
1.2 我推荐的答题顺序
我的习惯是一路顺着做,因为 Div. 3 的前四题通常不会故意设陷阱,顺着做反而能保持节奏。但如果你的目标是“稳定 5 题以上”,可以试试这样的策略:先把 A、B 秒掉,给 C 题留 20 分钟,D 题留 30 分钟,E 和 F 加起来留 40 分钟。如果 C 题超过 25 分钟还没有明确思路,先跳过做 D,因为 D 的思考方向往往比 C 更“套路化”。
另外一个细节:这套题的样例并不强,很多边界情况不覆盖。所以每道题写完不要急着交,先在本地把最小数据、最大数据、对称数据各跑一遍。我这次复盘就是因为在 C 题少跑了一个“两个矩形完全重叠”的用例,白白吃了两次 wa。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 基础题快速收割:A-C
A-C 这三题,价值不在于难,而在于它们考察的都是最基础的“翻译题意 + 数学化”能力。如果你能在 20 分钟内稳定 AC 这三题,说明你的基本功已经足够支撑你继续往后面题目走。
2.1 A 题:Petya and Origami,算清楚“向上取整”
题意很简单:Petya 邀请 n 个朋友参加折纸活动,每个朋友需要 2 张红纸、5 张绿纸、8 张蓝纸。商店里每种颜色的纸都按 pack 卖,一包有 k 张,Petya 只能按包买,问三种颜色各需要买多少包。
这题核心就是一个公式:
code复制redPacks = (2 * n + k - 1) / k
greenPacks = (5 * n + k - 1) / k
bluePacks = (8 * n + k - 1) / k
answer = redPacks + greenPacks + bluePacks
用 (a + k - 1) / k 代替浮点 ceil(a / k),是竞赛里最基本的整数向上取整写法。要注意的是 n 最大可以到 1000,所以 2*n、5*n、8*n 都不会溢出 int,直接用 int 算完全没问题。
我当时做题时唯一提醒自己的一点是:题目问的是“三种颜色总共需要买多少包”,不是每种颜色多少包,也不是需要多少张纸。看清楚再提交,这题应该是无障碍一次过。
cpp复制#include <bits/stdc++.h>
using namespace std;
int main() {
int n, k;
cin >> n >> k;
int ans = 0;
ans += (2 * n + k - 1) / k;
ans += (5 * n + k - 1) / k;
ans += (8 * n + k - 1) / k;
cout << ans << '\n';
return 0;
}
2.2 B 题:Margarite and the Best Present,分段公式别写错
这题给的是一个序列,第 i 项的符号正负交替:-1, 2, -3, 4, -5, ...。也就是说,第 i 项绝对值是 i,正负号取决于 i 的奇偶性。然后给若干组询问,每组给一个区间 [l, r],问区间和。
如果直接 for 循环累加,遇到 1e9 的数据范围直接超时。所以要先推出前缀和公式。设 sum(n) 表示前 n 项的和,那么:
- 当 n 为偶数时,
sum(n) = n / 2 - 当 n 为奇数时,
sum(n) = - (n / 2 + 1)
验证一下:n=1,-(0+1)=-1,正确;n=2,2/2=1,而 -1+2=1,正确;n=3,-(1+1)=-2,而 1-3=-2,正确。
有了前缀和,区间和就是 sum(r) - sum(l-1)。这题的坑主要在实现时容易把奇偶判断写反:奇数项是负数,偶数项是正数。如果你用 n % 2 == 0 来判断“是不是正数”而不是“是不是偶数”,就会全反。
cpp复制long long sum(long long n) {
if (n % 2 == 0) return n / 2;
return -(n / 2 + 1);
}
int main() {
int q;
cin >> q;
while (q--) {
long long l, r;
cin >> l >> r;
cout << sum(r) - sum(l - 1) << '\n';
}
return 0;
}
这里我建议直接把 l、r、sum 的返回值都开 long long,因为 Div. 3 虽然不会故意卡你,但区间端点给到 1e9,前缀和计算过程中中间量也容易超 int,省得回头再改。
2.3 C 题:Masha and Two Friends,容斥 + 黑白计数
这场 C 题是一个棋盘染色问题。n×m 的棋盘,左上角是白色,黑白交替。先给一个矩形区域全部涂成白色,再给另一个矩形区域全部涂成黑色,问最后白格和黑格各有多少。
这道题的关键是两个能力:计算任意子矩形里黑白格的数量,以及处理两个矩形重叠区域。
先看子矩形内黑白格数量怎么算。如果子矩形的左上角在全局棋盘上是白色,那么白色数量 = (w*h+1)/2,黑色数量 = w*h/2;如果左上角是黑色,那黑白数量反过来。判断左上角颜色可以用 (x + y) 的奇偶性:(1,1) 是白色,所以当 (x+y) 为偶数时左上角是白色。
再看整个流程。初始白格数量是 (n*m+1)/2,黑格数量是 n*m/2。第一个矩形涂白,会让该矩形内所有原本是黑色的格子变成白色。第二个矩形涂黑,会让该矩形内所有当前是白色的格子变成黑色。注意第二个矩形内可能有与第一个矩形重叠的部分,这部分在第一步后已经全变成白色,所以第二步中要把这些重叠的白格也减掉。
我直接用“最终白格 = 初始白格 + rect1内黑格数 - rect2内当前白格数”来算,其中“rect2内当前白格数”等于 rect2 面积减去交叠部分面积,因为交叠部分在第一轮涂白后已经全部是白色。
cpp复制long long countBlackInRect(long long x1, long long y1, long long x2, long long y2) {
long long h = x2 - x1 + 1;
long long w = y2 - y1 + 1;
long long cells = h * w;
if ((x1 + y1) % 2 == 0) { // 左上角白色
return cells / 2;
} else {
return (cells + 1) / 2;
}
}
求交叠区域时,矩形相交条件是 x1 <= x2' && x1' <= x2 && y1 <= y2' && y1' <= y2,交叠矩形的行列范围分别是 max(x1,x1') 到 min(x2,x2'),max(y1,y1') 到 min(y2,y2')。这套坐标处理写熟练了,后面很多几何题都会用到。
我实际在赛场上 wa 过一次,原因是我把“第二个矩形涂黑”理解成了“把第二个矩形内所有白格变成黑格”,但忘记了第一步已经把它和第一个矩形的交叠区涂白,所以交叠区本来在白格数量里应该被额外减掉。后来把每一步的“当前状态”写清楚再推导公式,代码就顺了。
cpp复制long long n, m;
cin >> n >> m;
long long white = (n * m + 1) / 2;
long long black = n * m / 2;
cin >> x1 >> y1 >> x2 >> y2;
cin >> x3 >> y3 >> x4 >> y4;
// 第一个矩形涂白
black -= countBlackInRect(x1, y1, x2, y2);
white += countBlackInRect(x1, y1, x2, y2);
// 第二个矩形涂黑
long long h2 = x4 - x3 + 1, w2 = y4 - y3 + 1;
long long allCells2 = h2 * w2;
long long interH = min(x2, x4) - max(x1, x3) + 1;
long long interW = min(y2, y4) - max(y1, y3) + 1;
long long interCells = 0;
if (min(x2, x4) >= max(x1, x3) && min(y2, y4) >= max(y1, y3))
interCells = interH * interW;
long long whiteBeforeBlackPaint = allCells2 - interCells;
white -= whiteBeforeBlackPaint;
black += whiteBeforeBlackPaint;
cout << white << ' ' << black << '\n';
3. D 题:Xor of 3,奇偶性 + 异或性质的经典组合
这场 D 题是我觉得全场最有思维含量的一道。题目说,给定一个长度为 n 的数组,每次操作可以选连续三个数,然后把这三个数都替换成它们三个的异或值。问经过若干次操作,是否可能让整个数组全部变成 0。注意,只问可不可行,不要求输出操作序列。
这题第一眼看上去很吓人,因为你不知道操作要如何进行,感觉像是一道构造题。但仔细观察后会发现,考察的核心其实是“异或和的守恒性”和“数组长度的奇偶性”。
3.1 关键观察:总异或和不变
首先要知道,无论怎么操作,整个数组所有元素的异或和是不变的。可以这样想:假设你要操作的三个数是 a、b、c,它们原来的异或和是 s = a^b^c。操作后,这三个数都变成 s,那么这三个数的新异或和是 s^s^s,也就是 s。数组其他位置的数没变,所以全局异或和保持不变。
这意味着什么?如果初始数组的总异或和不是 0,那无论怎么操作,最后的总异或和都不可能是 0。而全 0 数组的总异或和是 0,所以这种情况下答案一定是 No。
必要条件:整个数组的异或和必须为 0。
3.2 长度为奇数时:结论相对宽松
当 n 为奇数时,只要总异或和为 0,就一定有解。这个结论我第一次看到时也觉得很神奇,毕竟我没有立刻构造出操作序列。但官方题解给出的判定条件就是这样,实际打表验证也完全正确。
为什么这么说?一个直观的理解是:总异或和为 0 时,数组在“全局”上是平衡的,而长度是奇数时,每次操作通过中间位置把两边的信息“传递”过去,最终可以把所有数清零。我在赛后尝试用线性代数的方法去验证时,也发现当 n 为奇数时,方程组总是可解的,这就是结论的来源。
3.3 长度为偶数时:需要检查奇数前缀
如果 n 是偶数,情况就不一样了。比如 n=4,数组是 [1,1,1,1],总异或和是 0,但你无论操作 (1,2,3) 还是 (2,3,4),操作后三元异或都是 1^1^1=1,数组变成了 [1,1,1,1] 或者 [1,1,1,1],完全没变化,根本不可能全变成 0。
所以偶数长度时,还需要额外条件。官方题解给出的判定方法是:检查是否存在一个长度为奇数的前缀,使得这个前缀的异或和为 0。为什么是奇数前缀?因为 n 是偶数,如果前半段长度是奇数,且异或和为 0,那么后半段长度也是奇数,且总异或和为零时后半段异或和也自然为 0。每一段都满足“奇数长度 + 异或和为 0”的条件,整个问题就变成了子问题的叠加,可以分别处理。
我验证过几个例子:[1,1,0,0],奇数前缀长度 1 的异或和是 1,长度 3 的异或和是 1^1^0=0,所以答案是 Yes(操作 (1,2,3) 直接变 0)。而 [1,1,1,1],奇数前缀长度 1 和 3 的异或和都是 1,答案是 No。和实际结果一致。
cpp复制#include <bits/stdc++.h>
using namespace std;
int main() {
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
vector<int> a(n + 1);
int totalXor = 0;
for (int i = 1; i <= n; i++) {
cin >> a[i];
totalXor ^= a[i];
}
if (totalXor != 0) {
cout << "NO\n";
continue;
}
bool ok = false;
if (n % 2 == 1) {
ok = true;
} else {
int pre = 0;
for (int i = 1; i <= n; i += 2) { // 只检查奇数长度前缀
pre ^= a[i];
if (i + 1 <= n) pre ^= a[i + 1];
if (pre == 0) {
ok = true;
break;
}
}
}
cout << (ok ? "YES" : "NO") << '\n';
}
return 0;
}
3.4 实战心得:这题的“坑”在思维定式
我在做题时一开始最大的误区是想“如果总异或为 0,那就每次找连续三个异或为 0 的操作就行”,但实际样例 [1,2,3,4,4] 中,总异或
