枚举例题分析

例题 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. 对第 1 行的 323232 种按法之一:把第 1 行按其按法翻转(含对第 1、2 行的连带影响),记下按键次数;
  2. 对第 i=1,2,3,4i=1,2,3,4i=1,2,3,4 行:若第 iii 行第 jjj 列仍关着 ,就必须 按下第 i+1i+1i+1 行第 jjj 列(并翻转它的上下左右);累计按键次数;
  3. 递推完第 5 行后,检查整个灯阵是否全亮:
  • 全亮 ⇒\Rightarrow⇒ 用当前按键次数更新答案;
  • 不全亮 ⇒\Rightarrow⇒ 这个第 1 行方案不可行(因为第 5 行已经无法再被"下一行"修正);
  1. 在所有可行方案中取最小步数;若最小值 >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),多组询问直接查表。本题用第一种做法更简单。

易错点

  1. 输入格式 :第一行是数据组数 nnn,之后每组 5 行、每行 5 个字符,组与组之间有空行 ------用 cin >> string(会跳过空白)逐行读最省事,别用 getline 去纠结空行;
  2. "变亮"的方向 :本题要求全亮(全部为 1),不是全灭!照抄"费解的开关(全灭版)"会得到相反的结果;
  3. 第 5 行无法再修正:递推只能到第 5 行,所以必须检查第 5 行;忘记检查是最高频错误;
  4. 按键的连带范围 :翻转时上下左右越界要判(第 1 行没有"上面");
  5. 步数限制 :题目要求 ≤6\le6≤6 步,最小步数若大于 6 要输出 -1(不能输出那个步数);
  6. 多组数据要重置:每组数据都要从初始状态重新开始,灯阵数组、答案变量都要复位;
  7. 别把"按两次等于没按"漏掉:如果允许同一位置重复按,枚举空间就从 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,非常安全。

易错点

  1. 三个位置必须不同 :贪心找的时候每次都要 ++p,不能原地重复使用同一个位置;
  2. 前导零合法 :001 是合法的三位"串",要按"长度为 3 的字符串"处理,不能当整数 111 去重;
  3. 枚举答案时按 0∼9990\sim9990∼999 顺序枚举,天然去重,不需要 set;
  4. 判断可行性时不要用"字符计数"(那是错的,因为要按顺序取三个字符,且位置要不同------计数会允许把同一个字符用两次)。

这一类题的识别特征

"答案的取值范围很小 "(值域 ≤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(log⁡n)O(\log n)O(logn) 的局部更新"。

步骤(一种稳妥写法:维护排序数组)

  1. 预先排序得到 bbb(b[k] 是第 kkk 小的值),并且对每个位置记录它在 bbb 中的下标(排序前的数组用"值+下标"排序,保证稳定);
  2. 操作 1(把 axa_xax 从旧值改为 vvv):
  • 若 v>axv>a_xv>ax:它在有序数组中向右移,把所有满足旧值 <bk≤v<bk\le v<bk≤v 的位置左移一位;
  • 若 v<axv<a_xv<ax:向左移,对称处理;
  • 用一次线性扫描更新 b 与每个位置的名次映射;
  1. 操作 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′ 是修改次数。关键在于"询问不再需要枚举"。

易错点

  1. 稳定排序:值相同时"原来靠前的名次更小",所以排序的比较必须是"值升序,值相同则原下标升序";
  2. 名次的定义是 #{j:aj<ax}+1\#\{j:a_j<a_x\}+1#{j:aj<ax}+1(严格小于),相等的不算,这一条决定了移动区间是开区间还是闭区间;
  3. 修改时旧值要及时更新 a[x],否则下次修改会用错基准;
  4. 映射关系("位置 → 在有序数组中的下标")要双向维护,否则移动时找不到元素。

这一类题的启示

"每次只改动一点点"的问题,通常不需要每次重算整体。 先问"改动影响了谁",只枚举/更新受影响的部分------这与"换枚举对象"是同一思想(把整体枚举换成局部枚举)。


例题 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)"。

步骤(框架)

  1. 统计 cntvcntvcntv(v∈1,nv\in1,nv∈1,n);
  2. 枚举 t=1,2,...t=1,2,\dotst=1,2,...(直到 9t+1>n9t+1>n9t+1>n 之类的界);
  3. 预处理好 Sv=∑u≥vcntu⋅cntu+tSv=\sum_{u\ge v}cntu\cdot cntu+tSv=∑u≥vcntu⋅cntu+t 之类的后缀和(每个 ttt 做一次 O(n)O(n)O(n));
  4. 枚举 XbX_bXb(或 XaX_aXa),累加 (a,b)(a,b)(a,b) 与 (c,d)(c,d)(c,d) 的贡献到各计数数组;
  5. 最后对每个物品输出它在 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) 的实现细节)。

易错点

  1. 魔法值要互不相同(这是"四个物品"能构成阵的前提),统计时别把相同的值凑在一起;
  2. 两个式子的方向 :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 代进去时注意不等号方向与取整;
  3. 贡献要分别累加四次 (每个物品可能是 aaa、bbb、ccc、ddd),且"同一个物品不能同时担任两个角色";
  4. 后缀和的边界:v+t≤nv+t\le nv+t≤n 才有效;
  5. 答案可能很大,用 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⋅max⁡cnt≤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,含重复魔法值)上逐项比对,全部一致。

★本题的方法总结

"四个变量满足两个线性关系" 的题,通用解法是:

  1. 把关系整理成"若干个变量由一个参数决定";
  2. 枚举这个参数(它的取值往往很少);
  3. 剩下"成对"的部分用计数数组 + 前缀和批量处理。

这是"枚举 + 前缀和"最经典的一课。


例题 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)。

证明(抽屉原理,官方思路):

  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。
  2. 反证:假设所有余数两两不同。对 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)。
  3. 反之若 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。
  4. 若 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 即可(它是"结论",不依赖任何一段散文式证明)。

易错点

  1. 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;
  2. mmm 最小是 2(题目要求两个不同的数),nnn 可能小于 mmm;
  3. 判定方向要分清 :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;
  4. lcm(1..30)≈2.33\times10^{12} 超 32 位,用 64 位且先除后乘;
  5. T≤105T\le10^5T≤105:快读快写(scanf/关同步);
  6. 这是判定题,"是否存在"而不是"有多少对"------若题目改成计数,就要换方法了。

★本题的方法总结

枚举的前提是"枚举对象的规模可承受"。 当对象规模是 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⁡(max⁡N)⋅max⁡k⋅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),几乎瞬时。

易错点

  1. 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 是本题最大的坑(我第一版就栽在这里);
  2. 位置公式 同样涉及 n−1n-1n−1:n(n−1)2+k+1\frac{n(n-1)}{2}+k+12n(n−1)+k+1;
  3. N=1N=1N=1 特判(答案是 1);
  4. 组合数边乘边判溢出 (res > N 就返回),否则 C(109,2)C(10^9,2)C(109,2) 会爆 long long;
  5. 枚举列的上界 取 30 足够安全(C(2k,k)C(2k,k)C(2k,k) 增长极快);
  6. "第一次出现"要取所有列结果的最小值 (同一个 NNN 可能在多列出现,位置不同);
  7. 二分下界取 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 次轻量操作。由于循环体极简,实测可过;若担心常数可转置让外层枚举较小维。

易错点

  1. 必须是非负元素 才能用双指针!有负数时"和随 rrr 单调"不成立,双指针会错(此时只能用前缀和 + 二分/树状数组);
  2. while (sum > K) sum -= c[l++] 之后 lll 可能超过 r+1r+1r+1,计数 r-l+1 要保证非负;
  3. 答案是 long long(子矩阵个数可达 (500⋅501/22)\binom{500\cdot501/2}{2}(2500⋅501/2) 量级);
  4. 用"以 rrr 为右端点"的计数方式(ans += r-l+1),不要写成枚举左右端点;
  5. 枚举顺序:外层枚举 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)))。

易错点

  1. 桶的大小是 KKK :K≤106K\le10^6K≤106,每次循环清空 O(K)O(K)O(K) 会让复杂度变成 O(n2K)O(n^2K)O(n2K)------不能每次 memset 整个桶 !正确做法是每次重新开一个 vector(如上)或记录"用过哪些余数"再回滚;
  2. cnt[0] = 1 不能忘(空前缀,对应"从第 1 列开始"的区间);
  3. 列和与答案都要 long long(n=m=400n=m=400n=m=400,元素大时和会超 int);
  4. 取模后比较大小是错的------本题只判断"是否为 000",不要拿取模后的值比大小;
  5. 增量维护列和:上边界 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 种)。

★算法框架

  1. 算出层数 hhh(用上面的公式;sqrt 有浮点误差,要用循环校正到 h(h+1)/2=M+Nh(h+1)/2=M+Nh(h+1)/2=M+N);
  2. 枚举最底层的 2h2^h2h 种排布(可把一行压成一个整数/位掩码来枚举);
  3. 对每一种底层,逐层向上递推 :新的一层由下面一层"相邻两位是否相同"决定,同时统计出现过的 AAA 的个数;
  4. 若整座塔中 AAA 的个数恰为 MMM(那么 BBB 的个数必然恰为 NNN),答案 +1+1+1;
  5. 输出答案。

关于"逐层递推"的实现粒度 :用数组逐格点写最直白(每层 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,取决于机器与评测机状态)。课上写"分析"时说明清楚这一点即可。

易错点

  1. 高度公式 :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 会少一层,答案必错;
  2. 输入顺序是 MMM 在前、NNN 在后 (MMM 是 AAA 的人数);
  3. 底行摊开时的位序 :mask >> (h-i) & 1 与"取最低位再右移"这两种写法对应相反 的位序。位序本身不影响"整座塔 AAA 的总数"吗?影响(塔不是左右对称的数值结构),所以一旦选定一种写法,递推方向必须与之一致;
  4. 每层长度递减 :生成上一层时只用下面一层的前 len 位,循环边界要与下标约定统一("先算长度"还是"先减长度"要一致);
  5. 判定条件要同时满足 AAA 的个数 =M=M=M 且 BBB 的个数 =N=N=N:只判一个可能把互补的情形也算进来;
  6. 1 << h:本题 h≤21h\le21h≤21 仍在 int 范围内,但若数据范围放宽到 h≥31h\ge31h≥31 就必须用 1LL << h;
  7. 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(Nlog⁡N)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)log⁡max⁡(N,M))O(\max(N,M)\log\max(N,M))O(max(N,M)logmax(N,M)) 的排序/统计:

O(2min⁡(N,M)⋅max⁡(N,M)log⁡max⁡(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)。

易错点

  1. 答案必须 ≥0\ge0≥0 (允许空牌堆),ans 初值取 0,绝不能输出负数;
  2. XXX 或 YYY 可以为 0(子任务 1 就是 Y=0Y=0Y=0);
  3. 绝不能两侧都枚举 (2422^{42}242)------本题的全部难点就在"只枚举一侧";
  4. 平局无害:某张牌的贡献恰好为 0 时选不选都不影响最优值;
  5. 掩码用 int(M≤21M\le21M≤21),popcount 传 unsigned 避免符号问题;
  6. M<NM<NM<N 时转置矩阵 ,把指数从 2N2^N2N 降到 2M2^M2M(N=21,M=10N=21,M=10N=21,M=10 时差 2000 倍)。

★本题的方法总结

"两侧都要选"的问题,先问:固定一侧之后,另一侧的最优选择能不能直接算出来?

能 ⟹ 只枚举一侧 (2N+M→2min⁡2^{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+qlog⁡q)O(n^2+q\log q)O(n2+qlogq) 100 分

关键观察:

  1. 对固定的 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 的所有答案;
  2. 不同的询问如果左端点相同,可以复用这次扫描 (后面更大的 rrr 直接在已扫过的结果上继续);
  3. 所以把询问按 (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+qlog⁡q)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))。

易错点

  1. 必须 64 位 :ai≤1010a_i\le10^{10}ai≤1010,前缀和约 101410^{14}1014,答案 <247<2^{47}<247,用 int 必挂;
  2. q≤106q\le10^6q≤106:快读快写(否则光是 I/O 就超时);
  3. 排序必须 (l, r) 双关键字、r 升序 ;l 变化时必须清零重扫 (否则会把上一个 lll 的滚动值带进来);
  4. 区间和从 Sl=alS_l=a_lSl=al 开始(别忘了 k=lk=lk=l 的首项);
  5. 不要试图按二进制位拆解 :一个常见错误想法是"按位统计 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;
  6. 空间别开 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∣=⌊Nlcm⁡i∈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

易错点

  1. 位运算优先级 :mask & 1 == 1 错,要 (mask & 1) == 1;mask & (1<<i) 才是判断第 iii 位;
  2. 1<<n 溢出 :n≥31n\ge31n≥31 时用 1LL<<n 或改用其他方法;
  3. 子集枚举要覆盖到 mask=0 与 mask=(1<<n)-1(除非题目排除);
  4. sub = (sub-1) & mask 的循环必须用 do-while 或显式处理 sub==0,否则会少枚举或死循环;
  5. 容斥的符号别写反,lcm 别溢出。

相关推荐
繁华的地方不一定留下你的脚印1 小时前
C++线程如何安全停止与唤醒:std::jthread、stop_token与condition_variable
开发语言·c++
Navigator_Z1 小时前
LeetCode //C - 1269. Number of Ways to Stay in the Same Place After Some Steps
c语言·算法·leetcode
2601_965742221 小时前
短视频脚本创作方法分享:本地生活类账号的起步思路
大数据·人工智能·算法·ai·新媒体运营·生活
55873生态系统手记1 小时前
读完 55873 资产钱包的技术栈,我重新理解了 "合规钱包" 应该长什么样
算法·架构
旋生万物1 小时前
素数螺旋映射 $z_n=n^{1+i}$ 的角分布统计检验与零模型对比
大数据·前端·人工智能·算法·云原生·螺旋生成论·螺旋相位
HRTOS2 小时前
HRTOS Shell继续完善:优化printf缓冲区与字符串越界保护,国庆后随新版本更新
运维·单片机·嵌入式硬件·算法·51单片机
沫璃染墨2 小时前
《从零入门Linux系统篇(五十一):线程篇·四——pthread线程库详解:从线程创建到终止与分离》
linux·运维·服务器·开发语言·c++·系统架构·线程
shada2 小时前
CMake指定 MSVC 工具集与 Windows SDK
c++·windows
邵奈一2 小时前
当沉香之乡遇上 Seed-2.1-pro:我用 AI 给村子做了一个数字门面
算法·架构