例题 0 费解的开关(洛谷 P10449)
枚举什么 :第一行的按键方案 (25=322^5=3225=32 种) 优化 :其余各行被唯一确定,逐行递推即可
题意
5×55\times55×5 的灯阵,每盏灯有开(1)/关(0)两态。每一步可以改变某一盏灯 的状态,同时会连带翻转它上下左右相邻的灯 ("拉灯"的连锁反应)。给定初始状态,问能否在 666 步以内 把所有灯都变亮;能则输出最少步数,不能输出 -1。多组数据(0<n≤5000<n\le5000<n≤500)。
样例(节选):给定 00111/01011/10001/11010/11100 时答案为 3;11101/11101/11110/11111/11111 时为 2;某组只差一盏灯的状态时为 -1。
★枚举对象的选择
为什么不能枚举"所有按键组合" :5×5=255\times5=255×5=25 个位置各自按或不按,共 225≈3.3×1072^{25}\approx3.3\times10^7225≈3.3×107 种;每组数据都枚举一遍是不现实的(nnn 组就更不可能),而且题目只要求 ≤6\le6≤6 步,绝大多数组合根本不可能。
关键观察:考虑按完第 1 行之后------
第 1 行里任何一盏还关着的灯,只能由"它正下方那盏灯(第 2 行同列)"来改变(第 1 行自己的按钮已经决定不再按了,因为按两次等于没按,最优解中每个位置至多按一次)。
于是第 2 行的按键方案被第 1 行的状态唯一确定;同理第 3 行由第 2 行确定......一直到第 5 行。
结论:
只需枚举第 1 行的 25=32 种按键方案,其余各行逐行递推出来 \text{只需枚举第 1 行的 }2^5=32\text{ 种按键方案,其余各行逐行递推出来} 只需枚举第 1 行的 25=32 种按键方案,其余各行逐行递推出来
递推完第 5 行后检查"全亮"即可;不满足就说明这个第 1 行方案不可行。
配套的两个小结论 (都是剪枝与优化的依据):
① 每个位置至多按一次 ------按两次等于没按,按三次等于按一次,所以最优解中每个位置只有"按/不按"两种选择;
② 按键顺序无关------翻转变换满足交换律,所以只需关心"每个位置按了几次(0 或 1)",不需要关心先后。
★算法框架
- 对第 1 行的 323232 种按法之一:把第 1 行按其按法翻转(含对第 1、2 行的连带影响),记下按键次数;
- 对第 i=1,2,3,4i=1,2,3,4i=1,2,3,4 行:若第 iii 行第 jjj 列仍关着 ,就必须 按下第 i+1i+1i+1 行第 jjj 列(并翻转它的上下左右);累计按键次数;
- 递推完第 5 行后,检查整个灯阵是否全亮:
- 全亮 ⇒\Rightarrow⇒ 用当前按键次数更新答案;
- 不全亮 ⇒\Rightarrow⇒ 这个第 1 行方案不可行(因为第 5 行已经无法再被"下一行"修正);
- 在所有可行方案中取最小步数;若最小值 >6>6>6 或没有可行方案,输出
-1。
判定的充分性 :第 1 行固定后,第 2...5 行的按法是被迫 的(否则第 1...4 行永远无法变亮),所以枚举 323232 种第 1 行方案就覆盖了全部可能的合法解,不会漏。
复杂度
- 第 1 行 25=322^5=3225=32 种;
- 每种方案最多按 25×4=1282^5\times4=12825×4=128 次(实际上限是 25 次按键),每次按键翻转至多 5 盏灯;
单组数据约 32×25×5≈4×10332\times25\times5\approx4\times10^332×25×5≈4×103 次操作,极快。n≤500n\le500n≤500 组也在 10610^6106 量级,轻松通过。
另一种做法(也值得知道) :因为步数 ≤6\le6≤6,可以从终态(全亮)出发做 BFS ,把所有"≤6\le6≤6 步可达的状态"预处理出来(≤1+(251)+...\le1+\binom{25}{1}+\dots≤1+(125)+... 级别的状态数,实际远小于 2252^{25}225),多组询问直接查表。本题用第一种做法更简单。
易错点
- 输入格式 :第一行是数据组数 nnn,之后每组 5 行、每行 5 个字符,组与组之间有空行 ------用
cin >> string(会跳过空白)逐行读最省事,别用getline去纠结空行; - "变亮"的方向 :本题要求全亮(全部为
1),不是全灭!照抄"费解的开关(全灭版)"会得到相反的结果; - 第 5 行无法再修正:递推只能到第 5 行,所以必须检查第 5 行;忘记检查是最高频错误;
- 按键的连带范围 :翻转时上下左右越界要判(第 1 行没有"上面");
- 步数限制 :题目要求 ≤6\le6≤6 步,最小步数若大于 6 要输出
-1(不能输出那个步数); - 多组数据要重置:每组数据都要从初始状态重新开始,灯阵数组、答案变量都要复位;
- 别把"按两次等于没按"漏掉:如果允许同一位置重复按,枚举空间就从 2252^{25}225 变成 4254^{25}425(概念上),会白白浪费时间。
样例核对
| 初始状态 | 答案 |
|---|---|
00111 / 01011 / 10001 / 11010 / 11100 |
3 |
11101 / 11101 / 11110 / 11111 / 11111 |
2 |
| (某组只差一盏灯,需 >6>6>6 步) | -1 |
★本题的方法总结
这就是"枚举自由变量 + 约束递推 "的最经典形态:
① 找到真正自由的部分 (本题是第 1 行的 252^525 种按法);
② 证明其余部分被唯一确定 (每一行只能由下一行来"擦屁股");
③ 枚举自由部分 + 递推 + 最后校验。
它与例题 H(机器人塔)是同一个套路:底行/首行是自由变量,其余由规则唯一确定 。
识别信号 :题目出现"每一步会影响相邻元素""由相邻元素决定上一层"这类局部连锁规则时,先问"哪一行/哪一层是自由的"。
例题 A Lucky PIN(AtCoder sumitb2019 D)
枚举什么 :答案本身 (三位密码,10310^3103 种) 优化:贪心判断可行性
题意
给长度 NNN 的数字串 SSS。用 SSS 中三个不同位置 的字符按原顺序拼成三位数(允许前导 0,如 001),问能得到多少个不同的 三位数。
数据范围:4≤N≤3×1044\le N\le3\times10^44≤N≤3×104。
★枚举对象的选择(本手册最重要的示范)
朴素做法 :枚举位置 i<j<ki<j<ki<j<k,把拼出的数丢进 set 去重。
复杂度 O(N3)O(N^3)O(N3):N=3×104N=3\times10^4N=3×104 时约 4.5×10124.5\times10^{12}4.5×1012,完全不可行。
换对象 :答案只可能是 000∼999000\sim999000∼999,一共只有 100010001000 种可能!
与其枚举"哪些位置能拼出什么",不如枚举"密码是什么",再判断它能不能被拼出来。
判断方法(贪心) :对密码 d1d2d3d_1d_2d_3d1d2d3,从左到右在 SSS 中依次找这三个字符(每次都从上一个找到的位置之后继续找)。三个都找到 ⇒\Rightarrow⇒ 可拼出。
为什么贪心是对的 :为了让后面两个字符有尽可能多的选择余量,每一个字符都应该取最靠前的出现位置。若贪心都找不到,任何选法都找不到。
步骤
cpp
int ans = 0;
for (int x = 0; x <= 999; ++x) {
char d[3] = {char('0' + x / 100), char('0' + x / 10 % 10), char('0' + x % 10)};
int p = 0; // 在 S 中搜索的起始位置
for (int j = 0; j < 3; ++j) {
while (p < n && S[p] != d[j]) ++p; // 找最靠前的 d[j]
if (p == n) { p = -1; break; }
++p; // 下一个字符要从其后开始找
}
if (p != -1) ++ans;
}
复杂度
O(103⋅N)=3×107O(10^3\cdot N)=3\times10^7O(103⋅N)=3×107,非常安全。
易错点
- 三个位置必须不同 :贪心找的时候每次都要
++p,不能原地重复使用同一个位置; - 前导零合法 :
001是合法的三位"串",要按"长度为 3 的字符串"处理,不能当整数 111 去重; - 枚举答案时按 0∼9990\sim9990∼999 顺序枚举,天然去重,不需要
set; - 判断可行性时不要用"字符计数"(那是错的,因为要按顺序取三个字符,且位置要不同------计数会允许把同一个字符用两次)。
这一类题的识别特征
"答案的取值范围很小 "(值域 ≤106\le10^6≤106、字符串长度固定很小、状态数有限)而"输入的规模很大 "。
这时一律考虑枚举答案 + O(N)O(N)O(N) 判定。
例题 B 插入排序(洛谷 P7910,CSP-J 2021)
枚举什么 :只枚举被修改的那一个元素 优化:直接推导名次变化,不做整体重排
题意
给长度 nnn 的数组 aaa。两种操作共 QQQ 次:
1 x v:把 axa_xax 改成 vvv;2 x:询问 axa_xax 在数组中的稳定排名 (即 #{j:aj<ax}+1\#\{j: a_j<a_x\}+1#{j:aj<ax}+1)。
数据范围:n≤8000n\le8000n≤8000,Q≤2×105Q\le2\times10^5Q≤2×105。
枚举对象的选择
朴素做法 :每次询问时枚举所有 jjj 统计比 axa_xax 小的个数,O(n)O(n)O(n) 一次询问,总 O(nQ)=1.6×109O(nQ)=1.6\times10^9O(nQ)=1.6×109------偏大(且带排序更慢)。
关键观察 :操作 1 每次只改一个数。改完之后:
- 只有 axa_xax 这一个元素的排名会变;
- 其他元素的排名变化只取决于"它们与 axa_xax 的大小关系是否翻转",而这也可以用"改之前的名次 + 差值调整"推出来。
于是把"每次 O(n)O(n)O(n) 重排"变成"每次 O(1)O(1)O(1) 或 O(logn)O(\log n)O(logn) 的局部更新"。
步骤(一种稳妥写法:维护排序数组)
- 预先排序得到 bbb(
b[k]是第 kkk 小的值),并且对每个位置记录它在 bbb 中的下标(排序前的数组用"值+下标"排序,保证稳定); - 操作 1(把 axa_xax 从旧值改为 vvv):
- 若 v>axv>a_xv>ax:它在有序数组中向右移,把所有满足旧值 <bk≤v<bk\le v<bk≤v 的位置左移一位;
- 若 v<axv<a_xv<ax:向左移,对称处理;
- 用一次线性扫描更新
b与每个位置的名次映射;
- 操作 2:O(1)O(1)O(1) 输出记录的名次。
复杂度
每次修改 O(n)O(n)O(n),每次询问 O(1)O(1)O(1),总 O(nQ′)+O(Q)O(nQ')+O(Q)O(nQ′)+O(Q),其中 Q′Q'Q′ 是修改次数。关键在于"询问不再需要枚举"。
易错点
- 稳定排序:值相同时"原来靠前的名次更小",所以排序的比较必须是"值升序,值相同则原下标升序";
- 名次的定义是 #{j:aj<ax}+1\#\{j:a_j<a_x\}+1#{j:aj<ax}+1(严格小于),相等的不算,这一条决定了移动区间是开区间还是闭区间;
- 修改时旧值要及时更新
a[x],否则下次修改会用错基准; - 映射关系("位置 → 在有序数组中的下标")要双向维护,否则移动时找不到元素。
这一类题的启示
"每次只改动一点点"的问题,通常不需要每次重算整体。 先问"改动影响了谁",只枚举/更新受影响的部分------这与"换枚举对象"是同一思想(把整体枚举换成局部枚举)。
例题 C 魔法阵(洛谷 P2119,NOIP 2016 普及组)
枚举什么 :间隔参数 与一组位置 优化 :计数数组 + 前缀和把内层降为 O(1)O(1)O(1)
题意(简化表述)
给定 mmm 个物品,第 iii 个物品的"魔法值"为 XiX_iXi(1≤Xi≤n1\le X_i\le n1≤Xi≤n)。若存在四个物品的位置 a<b<c<da<b<c<da<b<c<d(指魔法值大小关系,且这四个魔法值互不相同)满足
Xb−Xa=2 (Xd−Xc),Xb−Xa<Xc−Xb3 X_b-X_a=2\,(X_d-X_c),\qquad X_b-X_a<\frac{X_c-X_b}{3} Xb−Xa=2(Xd−Xc),Xb−Xa<3Xc−Xb
则这四个物品构成一个魔法阵。统计每个物品作为 a,b,c,da,b,c,da,b,c,d 各出现多少次 。
数据范围:n≤15000n\le15000n≤15000,m≤40000m\le40000m≤40000。
★枚举对象的选择
直接枚举 a<b<c<da<b<c<da<b<c<d 是 O(m4)O(m^4)O(m4),绝对不行。观察两个式子:
设 t=Xd−Xct=X_d-X_ct=Xd−Xc,则:
- 第一个式子给出 Xb−Xa=2tX_b-X_a=2tXb−Xa=2t(固定的差);
- 第二个式子给出 Xc−Xb>3⋅2t=6tX_c-X_b>3\cdot2t=6tXc−Xb>3⋅2t=6t,即 ccc 要在 bbb 右边至少 6t+16t+16t+1 的位置上。
于是核心参数是 ttt,而且 ttt 的取值范围很小 (因为 Xd≤nX_d\le nXd≤n,而 2t=Xb−Xa2t=X_b-X_a2t=Xb−Xa、6t<Xc−Xb6t<X_c-X_b6t<Xc−Xb,三部分加起来不超过 nnn,所以 t≲n/9t\lesssim n/9t≲n/9)。
枚举 ttt 之后:
- (a,b)(a,b)(a,b) 是"距离为 2t2t2t 的一对 ",可以在值域上枚举 XaX_aXa 得到 Xb=Xa+2tX_b=X_a+2tXb=Xa+2t;
- (c,d)(c,d)(c,d) 是"距离为 ttt 的一对 ",且 Xc>Xb+6tX_c>X_b+6tXc>Xb+6t;
- 若记 cntvcntvcntv 为魔法值 vvv 的物品个数,则
- 对固定的 XbX_bXb,aaa 的贡献是 cntXb−2tcntX_b-2tcntXb−2t;
- ccc 的取值范围是 Xc>Xb+6tX_c>X_b+6tXc>Xb+6t 且 Xd=Xc+t≤nX_d=X_c+t\le nXd=Xc+t≤n,即 Xc∈Xb+6t+1, n−tX_c\inX_b+6t+1,\\ n-tXc∈Xb+6t+1, n−t;
- 这一段里所有 XcX_cXc 的"配对贡献"是 cntXc⋅cntXc+tcntX_c\cdot cntX_c+tcntXc⋅cntXc+t 的和 ------ 正好可以用"cntv⋅cntv+tcntv\cdot cntv+tcntv⋅cntv+t 的后缀和"O(1)O(1)O(1) 求出。
这就是"枚举 ttt + 前缀和/后缀和把内层 O(n)O(n)O(n) 降到 O(1)O(1)O(1)"。
步骤(框架)
- 统计 cntvcntvcntv(v∈1,nv\in1,nv∈1,n);
- 枚举 t=1,2,...t=1,2,\dotst=1,2,...(直到 9t+1>n9t+1>n9t+1>n 之类的界);
- 预处理好 Sv=∑u≥vcntu⋅cntu+tSv=\sum_{u\ge v}cntu\cdot cntu+tSv=∑u≥vcntu⋅cntu+t 之类的后缀和(每个 ttt 做一次 O(n)O(n)O(n));
- 枚举 XbX_bXb(或 XaX_aXa),累加 (a,b)(a,b)(a,b) 与 (c,d)(c,d)(c,d) 的贡献到各计数数组;
- 最后对每个物品输出它在 a/b/c/da/b/c/da/b/c/d 四个位置的次数。
复杂度
每个 ttt 需要 O(n)O(n)O(n) 的预处理与枚举,ttt 共有 O(n)O(n)O(n) 种,总 O(n2)O(n^2)O(n2)(n=15000n=15000n=15000 时约 2×1082\times10^82×108,注意常数与 O(n2)O(n^2)O(n2) 的实现细节)。
易错点
- 魔法值要互不相同(这是"四个物品"能构成阵的前提),统计时别把相同的值凑在一起;
- 两个式子的方向 :Xb−Xa=2(Xd−Xc)X_b-X_a=2(X_d-X_c)Xb−Xa=2(Xd−Xc) 与 Xb−Xa<(Xc−Xb)/3X_b-X_a<(X_c-X_b)/3Xb−Xa<(Xc−Xb)/3,把 ttt 代进去时注意不等号方向与取整;
- 贡献要分别累加四次 (每个物品可能是 aaa、bbb、ccc、ddd),且"同一个物品不能同时担任两个角色";
- 后缀和的边界:v+t≤nv+t\le nv+t≤n 才有效;
- 答案可能很大,用
long long。
数据范围与常数(为什么这个 O(n2)O(n^2)O(n2) 能过)
- 输入顺序是
n m(nnn 在前) ,随后一行 mmm 个魔法值------顺序写反是最常见的读入错误; - n≤15000n\le15000n≤15000、m≤40000m\le40000m≤40000、Xi≤nX_i\le nXi≤n(题目保证 XiX_iXi 近似随机,所以合法四元组很多);
- ttt 的枚举范围 :由 Xd−Xa≥9t+1≤nX_d-X_a\ge 9t+1\le nXd−Xa≥9t+1≤n 得 t≤(n−1)/9t\le(n-1)/9t≤(n−1)/9,故 ttt 只有约 n/9≈1666n/9\approx1666n/9≈1666 种;
- 内层总迭代次数 约 ∑t(n−2t)≈n2/9\sum_{t}(n-2t)\approx n^2/9∑t(n−2t)≈n2/9;n=15000n=15000n=15000 时实测量级约 2.2×1072.2\times10^72.2×107(我的逐项计算:22 212 77822\,212\,77822212778),在 1000 ms 内非常宽裕------这就是"O(n2/9)O(n^2/9)O(n2/9)"这个说法的来源 (社区里也常简写成 O(n2)O(n^2)O(n2),两种写法都有,指的是同一件事)。
溢出的精确边界 (决定用 int 还是 long long):
| 量 | 上界 | 是否安全 |
|---|---|---|
cnt[c] * cnt[d] |
m2=1.6×109<231−1m^2=1.6\times10^9<2^{31}-1m2=1.6×109<231−1 | 32 位安全 |
滚动累加量 s |
≤m⋅maxcnt≤m2=1.6×109\le m\cdot\max cnt\le m^2=1.6\times10^9≤m⋅maxcnt≤m2=1.6×109 | 32 位安全(实测最大约 4×1084\times10^84×108) |
| 最终 A/B/C/DA/B/C/DA/B/C/D | 题面保证每个输出 ≤109\le10^9≤109 | 32 位安全 |
等价写法 :把第一个式子代入第二个,可把条件 Xb−Xa<Xc−Xb3X_b-X_a<\dfrac{X_c-X_b}{3}Xb−Xa<3Xc−Xb 化成
Xc>4Xb−3XaX_c>4X_b-3X_aXc>4Xb−3Xa
(化除为乘,避免浮点与取整问题------这也是很多题解的写法,两者等价。)
完整代码(O(n2)O(n^2)O(n2),已对拍验证)
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 15005;
int n, m;
int cnt[MAXN]; // cnt[v] = 魔法值为 v 的物品个数
long long A[MAXN], B[MAXN], C[MAXN], D[MAXN]; // 按"魔法值"统计出现次数
long long S[MAXN], P[MAXN];
int main() {
scanf("%d %d", &n, &m);
vector<int> X(m + 1);
for (int i = 1; i <= m; ++i) { scanf("%d", &X[i]); ++cnt[X[i]]; }
// 枚举 t = Xd - Xc;由 Xb-Xa=2t 与 Xc-Xb>6t 可知 9t < n
for (int t = 1; 9 * t + 1 <= n; ++t) {
// S[v] = sum_{c >= v} cnt[c] * cnt[c + t]
S[n - t + 1] = 0;
for (int v = n - t; v >= 1; --v)
S[v] = S[v + 1] + 1LL * cnt[v] * cnt[v + t];
// P[b] = sum_{b' <= b} cnt[b'] * cnt[b' - 2t]
P[2 * t] = 0;
for (int b = 2 * t + 1; b <= n; ++b)
P[b] = P[b - 1] + 1LL * cnt[b] * cnt[b - 2 * t];
for (int b = 2 * t + 1; b <= n; ++b) {
int a = b - 2 * t;
if (cnt[a] && cnt[b]) {
// (a,b) 固定:c 必须 > b+6t 且 c+t <= n
int lo = b + 6 * t + 1;
if (lo <= n - t) {
long long s = S[lo];
A[a] += cnt[b] * s; // 每个值为 a 的物品都贡献 cnt[b]*s 次
B[b] += cnt[a] * s;
}
}
// (c,d) 固定:b < c-6t,即 b <= c-6t-1
int c = b + 6 * t + 1, d = c + t;
if (d <= n && cnt[c] && cnt[d]) {
long long s = P[b];
C[c] += cnt[d] * s;
D[d] += cnt[c] * s;
}
}
}
for (int i = 1; i <= m; ++i)
printf("%lld %lld %lld %lld\n", A[X[i]], B[X[i]], C[X[i]], D[X[i]]);
return 0;
}
验证记录 :上面的算法与"枚举全部四元组"的暴力在 500500500 组随机数据(n≤22n\le22n≤22,m≤10m\le10m≤10,含重复魔法值)上逐项比对,全部一致。
★本题的方法总结
"四个变量满足两个线性关系" 的题,通用解法是:
- 把关系整理成"若干个变量由一个参数决定";
- 枚举这个参数(它的取值往往很少);
- 剩下"成对"的部分用计数数组 + 前缀和批量处理。
这是"枚举 + 前缀和"最经典的一课。
例题 D 取模(洛谷 P8807,蓝桥杯 2022 国 C)
⚠️ 本题是"反例题":它看起来像枚举,实际是数论 + 抽屉原理,O(1)O(1)O(1) 判定,不需要枚举。
本手册把它放在这里,是为了讲清"枚举的边界在哪里"------第 8 章会专门讨论。
难度 :普及(黄) 来源 :蓝桥杯 2022 国赛 C 组 C 题 标签 :数论、枚举、鸽笼原理 限时/内存:1000 ms / 128 MB
题意
给定 n,mn,mn,m,问是否存在两个不同的数 x,yx,yx,y 满足 1≤x<y≤m1\le x<y\le m1≤x<y≤m 且 n mod x=n mod yn\bmod x=n\bmod ynmodx=nmody。多组询问,输出 Yes/No。
样例:1 2 → No;5 2 → No;999 99 → Yes。
数据范围:T≤105T\le10^5T≤105,1≤n≤1091\le n\le10^91≤n≤109,2≤m≤1092\le m\le10^92≤m≤109。
★为什么不能枚举
朴素做法:对每个 x∈1,mx\in1,mx∈1,m 算 n mod xn\bmod xnmodx,用桶判重。单次 O(m)O(m)O(m),m≤109m\le10^9m≤109、T≤105T\le10^5T≤105 ⟹ 完全不可能。
枚举对象的规模(10910^9109)远超可承受范围,此时必须换思路(而不是优化枚举)。
★关键结论
无解(
No) ⟺ \iff⟺ n mod 1, n mod 2,...,n mod mn\bmod 1,\ n\bmod 2,\dots,n\bmod mnmod1, nmod2,...,nmodm 两两不同 ⟺ \iff⟺ lcm(1..m) ∣ (n+1)\operatorname{lcm}(1..m)\ \big|\ (n+1)lcm(1..m) (n+1)。
证明(抽屉原理,官方思路):
- 设 L=lcm(1..m)L=\operatorname{lcm}(1..m)L=lcm(1..m)。因为每个 x∈1,mx\in1,mx∈1,m 都整除 LLL,所以 nnn 加减 LLL 不改变 n mod xn\bmod xnmodx,只需研究 n mod Ln\bmod LnmodL。
- 反证:假设所有余数两两不同。对 x∈1,mx\in1,mx∈1,m,xxx 个不同的 xxx(1..x1..x1..x)给出的余数必须互不相同,且都落在 0,x−10,x-10,x−1(只有 xxx 个可能值)⟹ 必须是 0,1,...,x−10,1,\dots,x-10,1,...,x−1 的一个排列 ⟹ 特别地 n mod x=x−1n\bmod x=x-1nmodx=x−1,即 x∣(n+1)x\mid(n+1)x∣(n+1) 对所有 x≤mx\le mx≤m 成立 ⟹ L∣(n+1)L\mid(n+1)L∣(n+1)。
- 反之若 L∣(n+1)L\mid(n+1)L∣(n+1):∀x≤m, n≡−1(modx)⇒n mod x=x−1\forall x\le m,\ n\equiv-1\pmod x\Rightarrow n\bmod x=x-1∀x≤m, n≡−1(modx)⇒nmodx=x−1,全部不同 ⟹
No。 - 若 L∤(n+1)L\nmid(n+1)L∤(n+1),则存在 x≤mx\le mx≤m 使 n mod x≤x−2n\bmod x\le x-2nmodx≤x−2。此时 n mod 1,...,n mod xn\bmod 1,\dots,n\bmod xnmod1,...,nmodx 这 xxx 个余数都落在 0,x−20,x-20,x−2 这 x−1x-1x−1 种 取值里 ⟹ 由抽屉原理必有重复 ⟹
Yes。
★一个立刻可用的推论 :lcm(1..23)=5354228880>109≥n\operatorname{lcm}(1..23)=5354228880>10^9\ge nlcm(1..23)=5354228880>109≥n,所以
m≥23 ⟹ 一定 Yes m\ge23\ \Longrightarrow\ \text{一定 } \texttt{Yes} m≥23 ⟹ 一定 Yes
于是只需预处理 lcm(1..i)\operatorname{lcm}(1..i)lcm(1..i)(i≤30i\le30i≤30)即可 O(1)O(1)O(1) 回答每个询问。
代码
cpp
long long L[31];
void init() { // L[i] = lcm(1..i)
L[0] = 1;
for (int i = 1; i <= 30; ++i) L[i] = L[i-1] / __gcd(L[i-1], (long long)i) * i;
}
string solve(long long n, long long m) {
if (m > 30) return "Yes"; // lcm 已超过 1e9,必定有重复
return ((n + 1) % L[m] == 0) ? "No" : "Yes";
}
复杂度
单次询问 O(1)O(1)O(1),总计 O(T)O(T)O(T),空间 O(1)O(1)O(1)。从 O(mT)O(mT)O(mT) 到 O(T)O(T)O(T),这是"换思路"而不是"优化枚举"带来的。
★自检工具:No 的显式形态
因为结论等价于"n+1n+1n+1 是 lcm(1..m)\operatorname{lcm}(1..m)lcm(1..m) 的倍数",所以对固定的 mmm,答案为 No 的 nnn 恰好构成一个等差数列:
| mmm | lcm(1..m)\operatorname{lcm}(1..m)lcm(1..m) | 答案为 No ⟺ \iff⟺ |
|---|---|---|
| 2 | 2 | n≡1(mod2)n\equiv1\pmod2n≡1(mod2)(即 nnn 为奇数) |
| 3 | 6 | n≡5(mod6)n\equiv5\pmod6n≡5(mod6) |
| 4 | 12 | n≡11(mod12)n\equiv11\pmod{12}n≡11(mod12) |
| 5, 6 | 60 | n≡59(mod60)n\equiv59\pmod{60}n≡59(mod60) |
| 22 | 232792560 | n≡232792559(mod232792560)n\equiv232792559\pmod{232792560}n≡232792559(mod232792560) |
| ≥23\ge23≥23 | >109>10^9>109 | 不可能(此时必为 Yes) |
用法 :写程序时把上表代几组进去自检(例如 n=5,m=2n=5,m=2n=5,m=2:555 是奇数 ⟹ No ✔;n=999,m=99n=999,m=99n=999,m=99:m≥23m\ge23m≥23 ⟹ Yes ✔)。
精确阈值与边界
- 阈值不是"大于 30 才一定 Yes",而是 m≥23m\ge23m≥23 就一定 Yes :
lcm(1..22)=232792560≤109,lcm(1..23)=5354228880>109≥n\operatorname{lcm}(1..22)=232792560\le10^9,\qquad \operatorname{lcm}(1..23)=5354228880>10^9\ge nlcm(1..22)=232792560≤109,lcm(1..23)=5354228880>109≥n
所以 m≥23m\ge23m≥23 时 L>nL>nL>n,而 0<n<L0<n<L0<n<L,不可能有 L∣(n+1)L\mid(n+1)L∣(n+1) ⟹ 必有重复。写if (m > 30) return "Yes";也对(更保守),但精确界是 23。 - 边界特例 :n=1n=1n=1 时 m=2⇒Nom=2\Rightarrow\texttt{No}m=2⇒No(1 mod 1=0,1 mod 2=11\bmod1=0,1\bmod2=11mod1=0,1mod2=1 不同),m≥3⇒Yesm\ge3\Rightarrow\texttt{Yes}m≥3⇒Yes;m>nm>nm>n 是合法输入(xxx 可以大于 nnn,此时 n mod x=nn\bmod x=nnmodx=n),不需要特判 ;n≥mn\ge mn≥m 也不需要特判。
- 性能 :O(1)O(1)O(1) 判定下,T=105T=10^5T=105 且 mmm 全落在 2,222,222,22(最坏,必经 lcm 分支)的实测约为 109 ms (限 1000 ms);这也解释了部分分设计------20% 的数据 10510^5105 次朴素枚举能过,50% 的数据已到 10910^9109 次、正好卡在 1000 ms 边缘。
★一个值得记住的方法论提示
洛谷讨论区存在质疑"好多题解的证明有问题 "的帖子(本次未能取得其正文),但判定结论本身已被两方独立穷举验证(数万至数十万组随机数据、零反例)。
这件事的教学意义 :做题时"结论正确"与"证明严谨"是两件事。
读到一份漂亮的推导时,最稳的验证方式仍是自己造小数据穷举对拍 ;本手册每道例题都配了这一步,正是为此。
就本题而言,如果对证明没把握,直接用同余判据
(n+1) % lcm(1..m) == 0即可(它是"结论",不依赖任何一段散文式证明)。
易错点
m > n/2之类的"直觉判据"是错的 (实测在 n,m≤300n,m\le300n,m≤300 范围内有 2 万组以上反例,例如 n=4,m=2n=4,m=2n=4,m=2 真值Yes):x>n/2x>n/2x>n/2 时 n mod x=n−xn\bmod x=n-xnmodx=n−x 确实互不相同,但证据不会落在那一段 ,不能据此判No;- mmm 最小是 2(题目要求两个不同的数),nnn 可能小于 mmm;
- 判定方向要分清 :m≥23⇒m\ge23\Rightarrowm≥23⇒
Yes(有重复),lcm(1..m)∣(n+1)⇒\operatorname{lcm}(1..m)\mid(n+1)\Rightarrowlcm(1..m)∣(n+1)⇒No; lcm(1..30)≈2.33\times10^{12}超 32 位,用 64 位且先除后乘;- T≤105T\le10^5T≤105:快读快写(
scanf/关同步); - 这是判定题,"是否存在"而不是"有多少对"------若题目改成计数,就要换方法了。
★本题的方法总结
枚举的前提是"枚举对象的规模可承受"。 当对象规模是 10910^9109 时,正确的动作是:
① 找等价刻画 (这里把"存在重复"变成"lcm\operatorname{lcm}lcm 是否整除 n+1n+1n+1");
② 找有限状态 (余数只有 xxx 种 ⟹ 抽屉原理);
③ 找阈值 (m≥23m\ge23m≥23 就必然有解)。
这三招都不在"枚举优化"的范畴里------识别"这题不该枚举"本身也是一种能力。
例题 E 杨辉三角形(洛谷 P8749,蓝桥杯 2021 省 B)
枚举什么 :列号 kkk(只有十几个) 优化:二分行号 + 组合数剪枝
题意
杨辉三角形按 从上到下、每行从左到右的顺序把数排成一行:
1, 1, 1, 1, 2, 1, 1, 3, 3, 1, 1, 4, 6, 4, 1, ...1,\ 1,\ 1,\ 1,\ 2,\ 1,\ 1,\ 3,\ 3,\ 1,\ 1,\ 4,\ 6,\ 4,\ 1,\ \dots1, 1, 1, 1, 2, 1, 1, 3, 3, 1, 1, 4, 6, 4, 1, ...
给定 NNN,求 NNN 第一次出现 在这个序列中的位置(从 111 开始编号)。
★枚举对象的选择
朴素做法是"逐行生成杨辉三角,边生成边找 NNN"------但 NNN 可达 10910^9109 量级,行数会极大。
关键观察 :杨辉三角里的每个数都是组合数。注意下标约定 :第 nnn 行(行号从 111 开始)从左数第 kkk 个数(kkk 从 000 开始)等于
C(n−1, k) C(n-1,\ k) C(n−1, k)
例如 n=3,k=1⇒C(2,1)=2n=3,k=1\Rightarrow C(2,1)=2n=3,k=1⇒C(2,1)=2 ✔;n=6,k=1⇒C(5,1)=5n=6,k=1\Rightarrow C(5,1)=5n=6,k=1⇒C(5,1)=5 ✔。
枚举列 kkk:
- k=0k=0k=0:只有值 111;
- k=1k=1k=1:C(n−1,1)=n−1C(n-1,1)=n-1C(n−1,1)=n−1,能取到所有正整数,但行号很大时位置很靠后;
- k=2k=2k=2:三角数 a(a−1)2\frac{a(a-1)}{2}2a(a−1),k=3k=3k=3:四面体数......
- 列号越靠右,该列能取到的值增长越快 :C(2k,k)C(2k,k)C(2k,k) 随 kkk 指数增长。当 k≥16k\ge16k≥16 时 C(2k,k)>109C(2k,k)>10^9C(2k,k)>109,因此只需枚举 k=0,1,...,15k=0,1,\dots,15k=0,1,...,15(约 16 个)。
枚举对象从"整张三角"缩减为"16 个列号",这就是本题的枚举优化。
★二分求行号
固定 kkk 后,C(a,k)C(a,k)C(a,k) 关于 aaa 单调不减 (k≥1k\ge1k≥1 时严格递增),二分求 C(a,k)=NC(a,k)=NC(a,k)=N:
cpp
long long N;
// 组合数,超过 N 就提前返回,避免溢出
long long C(long long a, long long k) {
if (k > a) return 0;
k = min(k, a - k);
long long res = 1;
for (long long i = 1; i <= k; ++i) {
res = res * (a - k + i) / i;
if (res > N) return res; // 剪枝
}
return res;
}
long long solve() {
if (N == 1) return 1; // 特判:1 出现在位置 1
long long ans = LLONG_MAX;
for (long long k = 1; k <= 30; ++k) {
long long lo = 2 * k, hi = N; // 第 k 列的最小值是 C(2k,k)
while (lo <= hi) {
long long mid = lo + (hi - lo) / 2;
long long c = C(mid, k);
if (c == N) {
long long n = mid + 1; // 行号 = 组合数下标 + 1
ans = min(ans, (n - 1) * n / 2 + k + 1); // 位置公式
break;
} else if (c < N) lo = mid + 1;
else hi = mid - 1;
}
}
if (ans == LLONG_MAX) ans = N * (N - 1) / 2 + 2; // 只能出现在第 1 列 C(n-1,1)
return ans;
}
★位置公式(本题最容易错的地方)
第 nnn 行之前共有 1+2+⋯+(n−1)=n(n−1)21+2+\dots+(n-1)=\frac{n(n-1)}{2}1+2+⋯+(n−1)=2n(n−1) 个数,所以第 nnn 行第 kkk 个数(kkk 从 0 起)的位置是
n(n−1)2+k+1 \boxed{\ \frac{n(n-1)}{2}+k+1\ } 2n(n−1)+k+1
两个下标约定必须同时搞清楚:
| 量 | 定义 | 例子 |
|---|---|---|
| 行号 nnn | 从 111 开始 | n=3n=3n=3 是 1 2 1 |
| 列号 kkk | 从 000 开始 | n=3,k=1n=3,k=1n=3,k=1 是 2 |
| 组合数 | C(n−1,k)C(n-1,k)C(n−1,k) | C(2,1)=2C(2,1)=2C(2,1)=2 |
| 位置 | n(n−1)2+k+1\frac{n(n-1)}{2}+k+12n(n−1)+k+1 | n=3,k=1⇒5n=3,k=1\Rightarrow5n=3,k=1⇒5 |
(已用程序逐值验证:把 (n,k)(n,k)(n,k) 与位置公式对照,并用"第 kkk 列二分"的算法与整张三角的首次出现位置比对 505 个值,结果完全一致。)
复杂度
O(log(maxN)⋅maxk⋅k)=O(30×16×16)O(\log(\max N)\cdot\max k\cdot k)=O(30\times16\times16)O(log(maxN)⋅maxk⋅k)=O(30×16×16),几乎瞬时。
易错点
- CCC 的下标 :第 nnn 行第 kkk 个数是 C(n−1,k)C(n-1,k)C(n−1,k),不是 C(n,k)C(n,k)C(n,k)------这个差 1 是本题最大的坑(我第一版就栽在这里);
- 位置公式 同样涉及 n−1n-1n−1:n(n−1)2+k+1\frac{n(n-1)}{2}+k+12n(n−1)+k+1;
- N=1N=1N=1 特判(答案是 1);
- 组合数边乘边判溢出 (
res > N就返回),否则 C(109,2)C(10^9,2)C(109,2) 会爆long long; - 枚举列的上界 取 30 足够安全(C(2k,k)C(2k,k)C(2k,k) 增长极快);
- "第一次出现"要取所有列结果的最小值 (同一个 NNN 可能在多列出现,位置不同);
- 二分下界取 2k2k2k:第 kkk 列的最小值是 C(2k,k)C(2k,k)C(2k,k),下标小于 2k2k2k 时该列没有有效值。
★本题的方法总结
"在一个按规则生成的巨大表里找某个值" :
① 把"位置"参数化(这里是一对 (n,k)(n,k)(n,k),等价于组合数下标);
② 找"位置关于值单调"的方向 → 二分 ;
③ 用增长上界砍掉绝大多数枚举对象(列号只枚举到 16)。
从"整张三角"到"16 个列号",这是枚举优化的极致体现。
例题 F 统计子矩阵(洛谷 P8783,蓝桥杯 2022 省 B)
枚举什么 :上下边界 + 双指针扫右边界 优化:单调性双指针
题意
给 n×mn\times mn×m 矩阵(元素非负)与 KKK,求元素和不超过 KKK 的子矩阵个数。
数据范围:1≤n,m≤5001\le n,m\le5001≤n,m≤500,0≤0\le0≤ 元素,KKK 较大。
★枚举对象与优化
第一步 :枚举上边界 uuu、下边界 ddd,把列和压成一维数组 ccc(非负)。
第二步 :问题变成"统计 ccc 中和 ≤K\le K≤K 的子区间个数"。
第三步(关键) :因为 ci≥0c_i\ge0ci≥0,所以固定左端点 lll 时,和随右端点 rrr 单调不减 。于是用双指针:rrr 从 1 到 mmm 只向右移动 ,lll 也向右收缩。整体 O(m)O(m)O(m)。
cpp
long long ans = 0;
for (int u = 1; u <= n; ++u) {
vector<long long> c(m + 1, 0);
for (int d = u; d <= n; ++d) {
for (int j = 1; j <= m; ++j) c[j] += a[d][j];
long long sum = 0;
int l = 1;
for (int r = 1; r <= m; ++r) {
sum += c[r];
while (sum > K) sum -= c[l++]; // 收缩左端点
ans += (r - l + 1); // 以 r 结尾的合法区间数
}
}
}
复杂度
O(n2m)O(n^2m)O(n2m),5002×500=1.25×108500^2\times500=1.25\times10^85002×500=1.25×108 次轻量操作。由于循环体极简,实测可过;若担心常数可转置让外层枚举较小维。
易错点
- 必须是非负元素 才能用双指针!有负数时"和随 rrr 单调"不成立,双指针会错(此时只能用前缀和 + 二分/树状数组);
while (sum > K) sum -= c[l++]之后 lll 可能超过 r+1r+1r+1,计数r-l+1要保证非负;- 答案是
long long(子矩阵个数可达 (500⋅501/22)\binom{500\cdot501/2}{2}(2500⋅501/2) 量级); - 用"以 rrr 为右端点"的计数方式(
ans += r-l+1),不要写成枚举左右端点; - 枚举顺序:外层枚举 uuu、ddd(O(n2)O(n^2)O(n2)),内层双指针(O(m)O(m)O(m))------把双指针放在较短的那一维能进一步提速。
与例题 G 的对照
| 例题 F | 例题 G | |
|---|---|---|
| 条件 | 和 ≤K\le K≤K | 和 ≡0(modK)\equiv0\pmod K≡0(modK) |
| 内层手段 | 双指针(用单调性) | 桶计数(用同余) |
| 前提 | 元素非负 | 无限制 |
| 共同点 | 都是"枚举上下边界 + 把一维问题 O(m)O(m)O(m) 解决" |
例题 G 入阵曲(洛谷 P3941)
枚举什么 :上下边界 优化:前缀和取模 + 桶计数(同余类)
题意
给 n×mn\times mn×m 的矩阵,问有多少个子矩阵的元素和是 KKK 的倍数 。
数据范围:n,m≤400n,m\le400n,m≤400,K≤106K\le10^6K≤106。
★枚举对象与优化
第一步(降维) :二维问题先"压成一维"------枚举上边界 uuu 与下边界 ddd,把第 u∼du\sim du∼d 行按列求和,得到一个长度 mmm 的一维数组 ccc。于是问题变成:
在 ccc 中统计有多少个子区间 l,rl,rl,r 使 ∑c≤\sum c\le∑c≤ 是 KKK 的倍数。
第二步(同余转化):
∑lrci≡0(modK) ⟺ prer≡prel−1(modK) \sum_{l}^{r} c_i\equiv0\pmod K\iff prer\equiv prel-1\pmod K l∑rci≡0(modK)⟺prer≡prel−1(modK)
其中 preprepre 是 ccc 的前缀和(对 KKK 取模后)。
第三步(桶计数把内层降为 O(1)O(1)O(1)) :扫一遍列,用一个大小为 KKK 的桶 cnt0..K−1cnt0..K-1cnt0..K−1 记录"当前余数出现过几次"。当扫描到位置 jjj 时,以 jjj 为右端点的合法区间数就是 cntpre\[j]cntpre\[j]cntpre\[j],然后 ++cnt[pre[j]]。
这就是"枚举一个端点 + 桶查表 ",单次内层 O(1)O(1)O(1)。
步骤
cpp
long long ans = 0;
for (int u = 1; u <= n; ++u) {
vector<long long> c(m + 1, 0);
for (int d = u; d <= n; ++d) {
for (int j = 1; j <= m; ++j) c[j] += a[d][j]; // 增量维护列和
vector<int> cnt(K, 0); cnt[0] = 1;
int s = 0;
for (int j = 1; j <= m; ++j) {
s = (s + c[j]) % K;
ans += cnt[s];
++cnt[s];
}
}
}
复杂度
O(n2m)O(n^2m)O(n2m)(枚举上下边界 O(n2)O(n^2)O(n2),每次 O(m)O(m)O(m))。4002×400=6.4×107400^2\times400=6.4\times10^74002×400=6.4×107,安全。
若 n≫mn\gg mn≫m 或 m≫nm\gg nm≫n,把矩阵转置,让"枚举对"落在较小的那一维上(O(min(n,m)2max(n,m))O(\min(n,m)^2\max(n,m))O(min(n,m)2max(n,m)))。
易错点
- 桶的大小是 KKK :K≤106K\le10^6K≤106,每次循环清空 O(K)O(K)O(K) 会让复杂度变成 O(n2K)O(n^2K)O(n2K)------不能每次
memset整个桶 !正确做法是每次重新开一个vector(如上)或记录"用过哪些余数"再回滚; cnt[0] = 1不能忘(空前缀,对应"从第 1 列开始"的区间);- 列和与答案都要
long long(n=m=400n=m=400n=m=400,元素大时和会超int); - 取模后比较大小是错的------本题只判断"是否为 000",不要拿取模后的值比大小;
- 增量维护列和:上边界 uuu 固定后,ddd 递增时累加 而不是重新求,省一个 O(n)O(n)O(n)。
★本题的方法总结
"子矩阵/子数组的计数"类问题四步走 :
① 前缀和降维 → ② 把条件写成"两个前缀之间的关系"(同余/大小) → ③ 用桶或哈希统计"前面有多少个满足关系的端点" → ④ 累加。
例题 F(统计子矩阵)是同一框架的"大小比较"版本。
例题 H 机器人塔(洛谷 P8644,蓝桥杯 2016 国 B)
枚举什么 :最底下一行的排布 (2h2^h2h 种) 优化 :其余各层由规则唯一确定,逐层递推即可
难度 :蓝桥杯 2016 年第七届国赛 B 组 限时/内存:1 s / 256 MB
题意
有两种服装 AAA、BBB 搭一座塔,共 hhh 层,最底层 hhh 个机器人,每往上一层少一个。组塔规则:
- AAA 只能站在 AAAAAA 或 BBBBBB 的肩上 (即:上层是 AAA ⟺ 下层相邻两个相同);
- BBB 只能站在 ABABAB 或 BABABA 的肩上 (上层是 BBB ⟺ 下层相邻两个不同)。
给定 AAA、BBB 的人数 M,NM,NM,N(保证存在 hhh 使 M+N=h(h+1)2M+N=\frac{h(h+1)}{2}M+N=2h(h+1)),求能搭出多少种不同的塔。
样例:1 2 → 3;3 3 → 4。
★两个必须先弄清的前提
① 塔高由总人数决定:M+N=h(h+1)2M+N=\dfrac{h(h+1)}{2}M+N=2h(h+1)(三角数),所以
h=⌊2(M+N)⌋h=\Big\lfloor\sqrt{2(M+N)}\Big\rfloorh=⌊2(M+N) ⌋
数据范围 0<M,N<N+M≤2310<M,N<N+M\le2310<M,N<N+M≤231 ⟹ h≤21h\le21h≤21(因为 21×222=231\frac{21\times22}{2}=231221×22=231)。
注意是 h(h+1)/2h(h+1)/2h(h+1)/2 而不是 h(h−1)/2h(h-1)/2h(h−1)/2 ------ 后者会少算一整层,是全题最容易踩的坑。
② 底行决定整座塔。 把规则反向读一遍:
- 上层某格是 AAA ⟺ 它脚下那两个相同;
- 上层某格是 BBB ⟺ 它脚下那两个不同。
所以只要最底层确定,第 2 层、第 3 层......直到塔尖全部被唯一确定,整座塔也就唯一确定。
于是"枚举整座塔"(22312^{231}2231 级别,不可能)被压缩成"枚举最底下一行 "(221≈2×1062^{21}\approx2\times10^6221≈2×106 种)。
★算法框架
- 算出层数 hhh(用上面的公式;
sqrt有浮点误差,要用循环校正到 h(h+1)/2=M+Nh(h+1)/2=M+Nh(h+1)/2=M+N); - 枚举最底层的 2h2^h2h 种排布(可把一行压成一个整数/位掩码来枚举);
- 对每一种底层,逐层向上递推 :新的一层由下面一层"相邻两位是否相同"决定,同时统计出现过的 AAA 的个数;
- 若整座塔中 AAA 的个数恰为 MMM(那么 BBB 的个数必然恰为 NNN),答案 +1+1+1;
- 输出答案。
关于"逐层递推"的实现粒度 :用数组逐格点写最直白(每层 O(层长)O(\text{层长})O(层长));把每层压成一个整数、用"相邻位的异或再取反"一次算出一整层,则每层只要 O(1)O(1)O(1) 次位运算,快一个数量级------后者更适合 h=21h=21h=21 的满数据。
复杂度
- 枚举底层:2h2^h2h 种,h≤21h\le21h≤21 ⟹ 约 2×1062\times10^62×106 种;
- 每种底层递推整座塔:共 h(h+1)2≤231\frac{h(h+1)}{2}\le2312h(h+1)≤231 个格子。
| 实现粒度 | 每种底层的代价 | 总量(h=21h=21h=21) |
|---|---|---|
| 逐格点 | O(h2)O(h^2)O(h2),约 231 次 | 约 4.8×1084.8\times10^84.8×108 次极简操作 |
| 每层压成整数(位运算) | O(h)O(h)O(h),约 21 次 | 约 4.4×1074.4\times10^74.4×107 次 |
两种写法在 1 s 内都能过,但位运算版余量充足,逐格点版比较贴时限(实测满数据约 1.9 s,取决于机器与评测机状态)。课上写"分析"时说明清楚这一点即可。
易错点
- 高度公式 :M+N=h(h+1)2⇒h=⌊2(M+N)⌋M+N=\frac{h(h+1)}{2}\Rightarrow h=\lfloor\sqrt{2(M+N)}\rfloorM+N=2h(h+1)⇒h=⌊2(M+N) ⌋。写成 h(h−1)/2h(h-1)/2h(h−1)/2 会少一层,答案必错;
- 输入顺序是 MMM 在前、NNN 在后 (MMM 是 AAA 的人数);
- 底行摊开时的位序 :
mask >> (h-i) & 1与"取最低位再右移"这两种写法对应相反 的位序。位序本身不影响"整座塔 AAA 的总数"吗?影响(塔不是左右对称的数值结构),所以一旦选定一种写法,递推方向必须与之一致; - 每层长度递减 :生成上一层时只用下面一层的前
len位,循环边界要与下标约定统一("先算长度"还是"先减长度"要一致); - 判定条件要同时满足 AAA 的个数 =M=M=M 且 BBB 的个数 =N=N=N:只判一个可能把互补的情形也算进来;
1 << h:本题 h≤21h\le21h≤21 仍在int范围内,但若数据范围放宽到 h≥31h\ge31h≥31 就必须用1LL << h;sqrt的浮点误差必须用循环校正(h(h+1)/2≠M+Nh(h+1)/2\neq M+Nh(h+1)/2=M+N 时 hhh 加一,直到相等)。
样例核对(已用程序复现)
| 输入 | hhh | 总格数 | 答案 |
|---|---|---|---|
1 2 |
2 | 3 | 3 |
3 3 |
3 | 6 | 4 |
h=3h=3h=3 时底层 23=82^3=823=8 种排布对应的"整座塔中 AAA 的个数"分布:
| 整座塔中 AAA 的个数 | 2 | 3 | 6 |
|---|---|---|---|
| 方案数 | 3 | 4 | 1 |
(合计 8=238=2^38=23;"AAA 的个数 =2=2=2"的 3 种正是样例 1 的答案,"AAA 的个数 =3=3=3"的 4 种正是样例 2 的答案。)
★本题的方法总结
凡是"由底层(或第一行)决定一切"的结构,枚举量就是 2底层长度2^{\text{底层长度}}2底层长度。
关键的一步是先证明"上层被唯一确定" ------做完这一步,问题就从"枚举整座塔"(22312^{231}2231 级别)降到"枚举底层"(2212^{21}221 级别)。
这与例题 0(费解的开关枚举第一行)是完全相同的思想 :枚举自由变量 + 约束递推。
推广信号:题目里出现"由下一行/下一层/相邻元素生成上一层""塔、三角形、棋盘翻转"这类描述时,先问一句"哪些量是自由的?",把它们枚举掉,其余用规则推出来。
例题 I 红蓝牌 / Karte(洛谷 P11475,COCI 2024/2025 #3 T2)
枚举什么 :一侧的取牌方案(2min(N,M)2^{\min(N,M)}2min(N,M) 个子集掩码) 优化 :枚举一侧 + 另一侧贪心(消掉一层 2N2^{N}2N)
难度 :普及(黄) 来源 :COCI 2024/2025 Round #3 T2「Karte」(满分 70) 标签 :枚举、COCI 限时/内存:1000 ms / 0.5 GB
题意
有 NNN 张红牌(编号 1..N1..N1..N)与 MMM 张蓝牌(1..M1..M1..M)。输入 NNN 行、每行一个长度 MMM 的 01 串,第 iii 行第 jjj 位为 1 表示"红牌 iii 与蓝牌 jjj"是一对好对。
从牌堆里选 rrr 张红牌、bbb 张蓝牌(可以为 0),定义
- 价值 www = 选中的红蓝牌之间"好对"的个数;
- 强度 =w−X⋅r−Y⋅b=w-X\cdot r-Y\cdot b=w−X⋅r−Y⋅b。
求最大强度(允许空牌堆,所以答案 ≥0\ge0≥0)。
样例:N=M=2,X=Y=0N=M=2,X=Y=0N=M=2,X=Y=0,好对为 11、10 ⟹ 3 ;N=M=3,X=1,Y=0N=M=3,X=1,Y=0N=M=3,X=1,Y=0,好对为 111,111,000 ⟹ 4 ;N=M=3,X=Y=1N=M=3,X=Y=1N=M=3,X=Y=1,好对为 111,101,011 ⟹ 1。
数据范围:1≤N,M≤211\le N,M\le211≤N,M≤21,0≤X,Y≤300\le X,Y\le300≤X,Y≤30。
★枚举对象的选择(本题的核心)
朴素做法 :同时枚举红牌子集 2N2^N2N 与蓝牌子集 2M2^M2M ⟹ 2422^{42}242,绝对不行。
关键观察(贡献可分离) :固定蓝牌的选择 BBB 后,每张红牌 iii 的贡献是
rhs(i)=∣{j∈B:(i,j) 是好对}∣ \operatorname{rhs}(i)=\big|\{j\in B:(i,j)\text{ 是好对}\}\big| rhs(i)= {j∈B:(i,j) 是好对}
而这些贡献互相独立(红牌之间没有交互),于是整体强度是
∑i∈Rrhs(i) − X⋅∣R∣ − Y⋅∣B∣ \sum_{i\in R}\operatorname{rhs}(i)\ -\ X\cdot|R|\ -\ Y\cdot|B| i∈R∑rhs(i) − X⋅∣R∣ − Y⋅∣B∣
固定 ∣R∣=r|R|=r∣R∣=r 时,最优的红牌选法就是"取 rhs\operatorname{rhs}rhs 最大的 rrr 张" (排序 + 前缀和,O(NlogN)O(N\log N)O(NlogN) 甚至 O(N)O(N)O(N))。
于是枚举量从 2N×2M2^N\times2^M2N×2M 降到 2M×N2^M\times N2M×N ------只枚举一侧。
镜像写法(更省) :枚举红牌子集 RRR,则蓝牌 jjj 的最优选择是"当 ∑i∈Rgood(i,j)>Y\sum_{i\in R}\text{good}(i,j)>Y∑i∈Rgood(i,j)>Y 时选它",值 =∑jmax(0,cj−Y)−X∣R∣=\sum_j\max(0,c_j-Y)-X|R|=∑jmax(0,cj−Y)−X∣R∣。
两种写法都对 ;实现时永远枚举较小的一维 (M<NM<NM<N 就把矩阵转置),指数为 2min(N,M)=2212^{\min(N,M)}=2^{21}2min(N,M)=221。
位运算加速
把每张红牌的"好对集合"压成一个 MMM 位掩码 good[i],则
cpp
int matched = good[i] & Bmask; // 与当前蓝牌选择的重叠
rhs[i] = __builtin_popcount((unsigned)matched); // O(1) 得到贡献
比逐位循环快 MMM 倍。
复杂度
枚举 2min(N,M)2^{\min(N,M)}2min(N,M) 个子集,每个子集做 O(max(N,M)logmax(N,M))O(\max(N,M)\log\max(N,M))O(max(N,M)logmax(N,M)) 的排序/统计:
O(2min(N,M)⋅max(N,M)logmax(N,M)) O\big(2^{\min(N,M)}\cdot\max(N,M)\log\max(N,M)\big) O(2min(N,M)⋅max(N,M)logmax(N,M))
N=M=21N=M=21N=M=21 时约 2×106×21×52\times10^6\times21\times52×106×21×5 量级的基本运算,1 秒内可过(注意常数:能 O(N)O(N)O(N) 就别排序)。空间 O(N+M)O(N+M)O(N+M)。
易错点
- 答案必须 ≥0\ge0≥0 (允许空牌堆),
ans初值取 0,绝不能输出负数; - XXX 或 YYY 可以为 0(子任务 1 就是 Y=0Y=0Y=0);
- 绝不能两侧都枚举 (2422^{42}242)------本题的全部难点就在"只枚举一侧";
- 平局无害:某张牌的贡献恰好为 0 时选不选都不影响最优值;
- 掩码用
int(M≤21M\le21M≤21),popcount传unsigned避免符号问题; - M<NM<NM<N 时转置矩阵 ,把指数从 2N2^N2N 降到 2M2^M2M(N=21,M=10N=21,M=10N=21,M=10 时差 2000 倍)。
★本题的方法总结
"两侧都要选"的问题,先问:固定一侧之后,另一侧的最优选择能不能直接算出来?
能 ⟹ 只枚举一侧 (2N+M→2min2^{N+M}\to2^{\min}2N+M→2min);再进一步,如果同侧元素之间贡献独立 ,还能把"选哪几个"变成"排序后取前 kkk 大"。
例题 J 区间操作 (opt)(洛谷 P10401,XSOI-R1)
枚举什么 :只枚举"被询问到的左端点 lll" 优化 :前缀和 + 询问离线排序 + 同 lll 复用
难度 :普及(黄,但数据规模很"硬") 来源 :XSOI Round 1 A 题 标签 :枚举、前缀和、离线处理、O2 优化 限时/内存 :2000 ms / 128 MB,25 个测试点
题意
给定数列 a1..ana_1..a_na1..an 与 qqq 次询问 (l,r)(l,r)(l,r),求
al ⊕ (al+al+1) ⊕ ⋯ ⊕ (al+al+1+⋯+ar) a_l\ \oplus\ (a_l+a_{l+1})\ \oplus\ \cdots\ \oplus\ (a_l+a_{l+1}+\dots+a_r) al ⊕ (al+al+1) ⊕ ⋯ ⊕ (al+al+1+⋯+ar)
即"从 lll 出发、终点取遍 l..rl..rl..r 的所有区间和的异或和 "(⊕\oplus⊕ 是异或)。
样例 1:6 1 / 1 1 4 5 1 4 / 1 6 → 18。
数据范围:1≤l≤r≤n≤1041\le l\le r\le n\le10^41≤l≤r≤n≤104,1≤q≤1061\le q\le10^61≤q≤106,0≤ai≤10100\le a_i\le10^{10}0≤ai≤1010。
★枚举对象的分析(三层递进,正是本手册的主题)
| 算法 | 枚举什么 | 复杂度 | 得分 |
|---|---|---|---|
| 暴力 | 枚举 lll、rrr、以及 l..rl..rl..r 内每个区间 | O(n2q)O(n^2q)O(n2q) 级别 | 13 分 |
| 前缀和 | 枚举 l,rl,rl,r,用前缀和 O(1)O(1)O(1) 取每个区间和 | O(nq)O(nq)O(nq):104×106=101010^4\times10^6=10^{10}104×106=1010 | 48 分 |
| 离线 + 只枚举被问到的 lll | 枚举"询问里出现过的左端点 lll" | O(n2+qlogq)O(n^2+q\log q)O(n2+qlogq) | 100 分 |
关键观察:
- 对固定的 lll,把 kkk 从 lll 扫到 rrr 时,区间和 Sk=prek−prel−1S_k=\text{pre}k-\text{pre}l-1Sk=prek−prel−1 可以滚动累加 (Sk=Sk−1+akS_k=S_{k-1}+a_kSk=Sk−1+ak),于是"扫一遍 O(r−l+1)O(r-l+1)O(r−l+1)"就能得到该 lll 的所有答案;
- 不同的询问如果左端点相同,可以复用这次扫描 (后面更大的 rrr 直接在已扫过的结果上继续);
- 所以把询问按 (l,r)(l,r)(l,r) 排序 ,按 lll 分组扫描:每个 lll 只扫到"该 lll 对应的最大 rrr"就停。
扫描总量 =∑被问到的 l(最大 r−l+1)≤n(n+1)2≈5×107=\sum_{\text{被问到的 }l}(\text{最大 }r-l+1)\le\frac{n(n+1)}{2}\approx5\times10^7=∑被问到的 l(最大 r−l+1)≤2n(n+1)≈5×107------正好卡在 2 秒内。
步骤
cpp
struct Q { int l, r, id; };
sort(qs.begin(), qs.end(), [](const Q& x, const Q& y) {
return x.l != y.l ? x.l < y.l : x.r < y.r; // ① (l,r) 双关键字升序
});
long long cur = 0; int pos = 0, curL = -1;
for (auto& q : qs) {
if (q.l != curL) { cur = 0; pos = q.l - 1; curL = q.l; } // ② 换 l 就清零重扫
while (pos < q.r) { ++pos; cur += a[pos]; ans[q.id] ^= cur; } // ③ 滚动累加并异或
}
复杂度
时间 O(n2+qlogq)O(n^2+q\log q)O(n2+qlogq),空间 O(q)O(q)O(q)(注意:预存所有区间和是 O(n2)O(n^2)O(n2),10810^8108 个 long long ≈ 763 MB,必然 MLE ------本题的 (opt) 就是逼你把空间从 O(n2)O(n^2)O(n2) 降到 O(q)O(q)O(q))。
易错点
- 必须 64 位 :ai≤1010a_i\le10^{10}ai≤1010,前缀和约 101410^{14}1014,答案 <247<2^{47}<247,用
int必挂; - q≤106q\le10^6q≤106:快读快写(否则光是 I/O 就超时);
- 排序必须 (l, r) 双关键字、r 升序 ;
l变化时必须清零重扫 (否则会把上一个 lll 的滚动值带进来); - 区间和从 Sl=alS_l=a_lSl=al 开始(别忘了 k=lk=lk=l 的首项);
- 不要试图按二进制位拆解 :一个常见错误想法是"按位统计 SkS_kSk 的奇偶",这是错的 ------Sk−Sl−1S_k-S_{l-1}Sk−Sl−1 的第 bbb 位受低位借位影响。反例:aaa 全为 1 时询问 (1,4)(1,4)(1,4) 与 (2,5)(2,5)(2,5) 的项完全相同(答案都是 4),但逐位思路会给出 4 和 0;
- 空间别开 O(n2)O(n^2)O(n2)(见上)。
★本题的方法总结
枚举量的削减有三个层次 :
① 把"区间内逐项"换成"前缀和 O(1)O(1)O(1)";
② 把"所有 lll"换成"只枚举被问到的 lll "(离线 + 只做必要的工作);
③ 把"每个 lll 都从头扫"换成"同 lll 复用、r 升序延伸"。
这三步都是"枚举优化",改动都在枚举对象与枚举顺序上,而不是算法本身。
例题 L(补充经典) 子集枚举四模型
枚举什么 :用二进制掩码表示"选/不选" 优化:位运算 + 折半 + 容斥
模型 1:枚举 + 模拟(开关灯、选或不选)
有 nnn 个开关(n≤20n\le20n≤20),每个开关控制若干盏灯。问是否存在方案让所有灯熄灭。
cpp
for (int mask = 0; mask < (1 << n); ++mask) {
int state = 0;
for (int i = 0; i < n; ++i)
if (mask >> i & 1) state ^= ctrl[i]; // ctrl[i] 是第 i 个开关影响的灯的掩码
if (state == 0) { /* 找到方案 */ }
}
复杂度 O(2n⋅n)O(2^n\cdot n)O(2n⋅n)。要点 :把"每个开关影响的集合"也压成掩码,用异或模拟翻转,比逐个灯判断快 mmm 倍。
模型 2:枚举 + 判重(统计不同结果)
给定 nnn 个数字(可重复),问能拼出多少个不同的结果。
- 若"拼"是把子集的元素求和 :枚举 2n2^n2n 个子集求和的集合,去重;
- 若"拼"是排列 :那是 O(n!)O(n!)O(n!),要用
next_permutation或 DFS; - 判重手段 :
std::set/std::vector+sort+unique。
cpp
vector<int> v;
for (int mask = 0; mask < (1 << n); ++mask) {
int s = 0;
for (int i = 0; i < n; ++i) if (mask >> i & 1) s += a[i];
v.push_back(s);
}
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());
cout << v.size() << endl;
注意"空集"算不算 :取决于题目("至少选一个"要去掉
mask=0)。
模型 3:枚举 + 递推(自由变量 + 约束)
见例题 0(费解的开关)与例题 H(机器人塔):只枚举"自由变量",其余部分由约束递推出来。
这是子集枚举最重要的推广:2302^{30}230 的状态里,真正独立的往往只有一小部分。
模型 4:枚举子集 + 容斥
求 1∼N1\sim N1∼N 中"能被 p1p_1p1 或 p2p_2p2 或......整除"的个数。
由容斥原理:
∣⋃i∈SAi∣=∑∅≠S⊆{1..k}(−1)∣S∣+1∣⋂i∈SAi∣,∣⋂i∈SAi∣=⌊Nlcmi∈Spi⌋ \Big|\bigcup_{i\in S}A_i\Big|=\sum_{\emptyset\ne S\subseteq\{1..k\}}(-1)^{|S|+1}\Big|\bigcap_{i\in S}A_i\Big|,\qquad \Big|\bigcap_{i\in S}A_i\Big|=\Big\lfloor\frac{N}{\operatorname{lcm}_{i\in S}p_i}\Big\rfloor i∈S⋃Ai =∅=S⊆{1..k}∑(−1)∣S∣+1 i∈S⋂Ai , i∈S⋂Ai =⌊lcmi∈SpiN⌋
cpp
long long ans = 0;
for (int mask = 1; mask < (1 << k); ++mask) {
long long l = 1; int bits = 0;
for (int i = 0; i < k; ++i) if (mask >> i & 1) { l = lcm(l, p[i]); ++bits; }
if (l > N) continue; // 剪枝:lcm 超过 N 就没有贡献
ans += ((bits & 1) ? 1 : -1) * (N / l);
}
要点:
- 枚举 SSS 的 2k−12^k-12k−1 种非空组合,符号由 ∣S∣|S|∣S∣ 的奇偶决定;
lcm可能溢出/超过 NNN,要及时break剪枝(否则lcm会爆long long)。
模型 5(进阶):枚举固定大小子集与折半枚举
- 固定大小 kkk 的子集:用 Gosper's Hack(见第四章 4.4);
- nnn 到 40 :折半枚举(4.5),两半各 2202^{20}220,排序后配对。
子集枚举的复杂度速查
| 形态 | 复杂度 | 可承受 nnn |
|---|---|---|
| 枚举所有子集 + O(n)O(n)O(n) 处理 | O(2nn)O(2^n n)O(2nn) | ≤22\le22≤22 |
| 枚举所有子集 + O(1)O(1)O(1) 递推处理 | O(2n)O(2^n)O(2n) | ≤25\le25≤25 |
| 枚举所有子集的所有子集 | O(3n)O(3^n)O(3n) | ≤13∼15\le13\sim15≤13∼15 |
| 折半枚举 | O(2n/2log)O(2^{n/2}\log)O(2n/2log) | ≤40\le40≤40 |
| 容斥枚举 | O(2k)O(2^k)O(2k) | k≤20k\le20k≤20 |
易错点
- 位运算优先级 :
mask & 1 == 1错,要(mask & 1) == 1;mask & (1<<i)才是判断第 iii 位; 1<<n溢出 :n≥31n\ge31n≥31 时用1LL<<n或改用其他方法;- 子集枚举要覆盖到
mask=0与mask=(1<<n)-1(除非题目排除); sub = (sub-1) & mask的循环必须用do-while或显式处理sub==0,否则会少枚举或死循环;- 容斥的符号别写反,
lcm别溢出。