枚举及其优化学习手册

枚举及其优化学习手册

配套题目(按"怎么优化"分类,共 12 道)

编号 题目 来源 枚举什么 怎么优化
0 费解的开关 洛谷 P10449 第一行的按键方案 ( 2 5 2^5 25) 其余各行被唯一确定,逐行递推
A Lucky PIN AtCoder sumitb2019 D 答案本身 ( 10 3 10^3 103)而非下标三元组 改变枚举对象 + 贪心判定
B 插入排序 洛谷 P7910(CSP-J 2021) 只枚举被修改的那一个数 局部更新名次,询问 O ( 1 ) O(1) O(1)
C 魔法阵 洛谷 P2119(NOIP 2016 普及组) 间隔 t t t + 一组位置 计数数组/前缀和把内层降 O ( 1 ) O(1) O(1)
E 杨辉三角形 洛谷 P8749(蓝桥杯 2021 省 B) 列号 k k k(只有约 16 个) 二分行号 + 组合数剪枝
F 统计子矩阵 洛谷 P8783(蓝桥杯 2022 省 B) 上下边界 双指针(用单调性)
G 入阵曲 洛谷 P3941 上下边界 前缀和取模 + 桶计数(同余类)
H 机器人塔 洛谷 P8644(蓝桥杯 2016 国 B) 最底下一行的排布 ( 2 h 2^h 2h, h ≤ 21 h\le21 h≤21) 上层由规则唯一确定,逐层递推
I 红蓝牌 / Karte 洛谷 P11475(COCI 2024/2025 #3) 一侧子集( 2 min ⁡ ( N , M ) 2^{\min(N,M)} 2min(N,M)) 另一侧排序取前 r r r 大
J 区间操作 (opt) 洛谷 P10401(XSOI-R1) 只枚举被询问到的左端点 前缀和 + 离线排序 + 同 l l l 复用
L 子集枚举专题(我补充) 补充经典 二进制掩码 位运算 / 折半 / 容斥
D 取模 洛谷 P8807(蓝桥杯 2022 国 C) 不是枚举题(数论 + 抽屉原理) O ( 1 ) O(1) O(1) 判定

目录

  • [第一章 枚举算法简介](#第一章 枚举算法简介)
  • [第二章 枚举对象的选择](#第二章 枚举对象的选择)
  • [第三章 枚举的优化](#第三章 枚举的优化)
  • [第四章 子集枚举](#第四章 子集枚举)
  • [第五章 识别手册与易错点](#第五章 识别手册与易错点)
  • [第六章 例题分析](#第六章 例题分析)
  • [第七章 训练建议](#第七章 训练建议)
  • [第八章 反例与对照:什么时候不该枚举](#第八章 反例与对照:什么时候不该枚举)
    • [例题 0 费解的开关](#例题 0 费解的开关)
    • [例题 A Lucky PIN](#例题 A Lucky PIN)
    • [例题 B 插入排序](#例题 B 插入排序)
    • [例题 C 魔法阵](#例题 C 魔法阵)
    • [例题 D 取模](#例题 D 取模)
    • [例题 E 杨辉三角形](#例题 E 杨辉三角形)
    • [例题 F 统计子矩阵](#例题 F 统计子矩阵)
    • [例题 G 入阵曲](#例题 G 入阵曲)
    • [例题 H 机器人塔](#例题 H 机器人塔)
    • [例题 I 红蓝牌 / Karte](#例题 I 红蓝牌 / Karte)
    • [例题 J 区间操作 (opt)](#例题 J 区间操作 (opt))
    • [例题 L 子集枚举四模型(补充)](#例题 L 子集枚举四模型(补充))

第一章 枚举算法简介

1.1 枚举是"暴力",但暴力不等于笨

枚举(穷举)是最朴素的算法思想:

把所有可能的答案 / 所有可能的中间状态一个不漏地列出来,逐个检查,取最优或计数。

它的优点极其重要:

  • 正确性天然:只要"枚举范围覆盖了全部可能"且"检查条件写对",答案就不会错;
  • 是验证其他算法的工具:任何复杂算法都可以先用枚举写一个暴力版本,用来对拍;
  • 是许多正解的前半段 :大量"正解"其实是"枚举 + 优化"------枚举部分不变,只是把检查/统计从 O ( n ) O(n) O(n) 降到 O ( 1 ) O(1) O(1)。

所以学习枚举有两个层次:

层次 目标 训练方法
第一层 能写出不重不漏的暴力 先想清楚"枚举的集合是什么",再写循环
第二层 能算清复杂度 并换枚举对象 / 优化检查 每写一个暴力,都问"哪些循环是浪费的"

1.2 枚举的三要素

任何枚举方案都由三件事决定:

  1. 枚举对象:你循环的是什么?(下标?值?方案?状态?子集?)
  2. 枚举范围:每个变量的上下界(决定"漏没漏");
  3. 判定/统计方式:每个枚举出来的对象,你怎么判断它是否合法、以及它对答案贡献多少。

写代码的顺序永远是:先写清楚这三件事(可以在纸上),再动手。

1.3 复杂度就是一切:先看数据范围

考场上最有效的习惯是:先看数据范围,反推可以承受的复杂度。

n n n(或值域) 可以承受的复杂度 常见枚举形态
n ≤ 10 n\le10 n≤10 O ( n ! ) O(n!) O(n!)、 O ( 2 n ⋅ n 2 ) O(2^n\cdot n^2) O(2n⋅n2) 全排列、子集
n ≤ 20 n\le20 n≤20 O ( 2 n ) O(2^n) O(2n)、 O ( 2 n ⋅ n ) O(2^n\cdot n) O(2n⋅n) 子集枚举(状压 DP 的雏形)
n ≤ 100 n\le100 n≤100 O ( n 4 ) O(n^4) O(n4) 四重循环
n ≤ 500 n\le500 n≤500 O ( n 3 ) O(n^3) O(n3) 三重循环
n ≤ 5000 n\le5000 n≤5000 O ( n 2 ) O(n^2) O(n2) 双重循环
n ≤ 2 × 10 5 n\le2\times10^5 n≤2×105 O ( n log ⁡ n ) O(n\log n) O(nlogn)、 O ( n n ) O(n\sqrt n) O(nn ) 枚举 + 哈希/前缀和/双指针
n ≤ 10 6 n\le10^6 n≤106 O ( n log ⁡ n ) O(n\log n) O(nlogn)、 O ( n ) O(n) O(n) 枚举 + O ( 1 ) O(1) O(1) 查询

口诀 :算一算 10 8 10^8 108。每秒大约能跑 10 8 10^8 108 次简单运算 ,把 "循环层数 × 每层规模" 估成 10 8 10^8 108 以内就基本安全(常数大的算法要再打个折)。

例: n = 5000 n=5000 n=5000 的双重循环是 2.5 × 10 7 2.5\times10^7 2.5×107,安全;三重循环 1.25 × 10 11 1.25\times10^{11} 1.25×1011,必爆------必须想办法砍掉一层,这就是第三章的全部内容。

1.4 "不重不漏"是枚举的生命线

写暴力最容易犯的两个错:

  • 重复 :同一个方案被算多次(例如把 ( i , j ) (i,j) (i,j) 和 ( j , i ) (j,i) (j,i) 都算了一遍,而题目只要无序对);
  • 遗漏 :边界没算到(例如上界写成 <n 而答案可能是 n n n;或者"空集"这种退化情况没枚举)。

三个自检问题(写完暴力先问自己):

  1. 我的循环真的覆盖了所有合法方案吗?把最小、最大、退化(空/全选/相等)的情况代进去试一遍;
  2. 有没有重复计数 ?需要去重吗(排序去重 / 强制 i < j i<j i<j / 规定字典序最小者代表整个方案)?
  3. 计数类题目结果需要取模 吗?答案会不会超 int?

第二章 枚举对象的选择

这是本手册最重要的一章。同一道题,换一个枚举对象,复杂度可能从 O ( n 3 ) O(n^3) O(n3) 掉到 O ( 10 3 ) O(10^3) O(103)。 例题 A(Lucky PIN)就是最经典的示范。

2.1 三层法:拿到题先问三个问题

  1. 我枚举的到底是什么? 写下来("我在枚举下标三元组"或"我在枚举密码本身")。
  2. 有没有别的等价对象,规模更小? 常见替换方向:
  • 下标三元组 → \to → 值本身 (例题 A: O ( n 3 ) → O ( 10 3 ) O(n^3)\to O(10^3) O(n3)→O(103));
  • 枚举方案 → \to → 枚举方案的某个关键参数 (例题 C:枚举间隔 d d d);
  • 枚举"每个元素选不选" → \to → 枚举互补的部分(更小的一边);
  • 枚举 O ( n 2 ) O(n^2) O(n2) 对 → \to → 枚举其中一个 + 查询另一个(例题 F/G)。
  1. 这个对象的"规模"和"数据范围"哪个更小? 通常选规模更小的那个(值域小就枚举值域, n n n 小就枚举 n n n)。

2.2 例题 A 的启示:枚举"答案"而不是"位置"

题目:给长度 N N N 的数字串 S S S,求"用 S S S 的三个不同位置拼成的三位数"有多少个不同的值。

朴素做法 :枚举 i < j < k i<j<k i<j<k, O ( N 3 ) O(N^3) O(N3), N ≤ 3 × 10 4 N\le3\times10^4 N≤3×104 时约 4.5 × 10 12 4.5\times10^{12} 4.5×1012,必爆。

换对象 :答案只可能是 000 ∼ 999 000\sim999 000∼999 这 1000 1000 1000 个数!所以

枚举密码本身( 1000 1000 1000 种),判断"它能不能被拼出来"。

判断方法:对密码 d 1 d 2 d 3 d_1d_2d_3 d1d2d3,在 S S S 中贪心地依次找 d 1 , d 2 , d 3 d_1,d_2,d_3 d1,d2,d3(每次都从上一个位置之后找),三个都能找到就说明可行。

复杂度 O ( 10 3 ⋅ N ) O(10^3\cdot N) O(103⋅N)。从"枚举位置"换成"枚举答案"是枚举优化的第一招,也是最常考的一招。

2.3 降低枚举维数:把一维交给预处理

很多题的枚举对象本身没错,只是"维数太高"。降维的三种典型手段:

  1. 前缀和/二维前缀和:把"枚举一段区间"变成"枚举两个端点"甚至"枚举一个端点"(例题 F、G);
  2. 哈希 / 桶计数 :把"再枚举一个对象去配对"变成 O ( 1 ) O(1) O(1) 查表(例题 G、L);
  3. 单调性 / 双指针 :当"随左端点右移,右端点也单调右移"时,内层循环的规模从 O ( n ) O(n) O(n) 摊还到 O ( 1 ) O(1) O(1)(例题 F)。

判断标准 :若内层循环的代码是"检查某个条件",就问"这个条件能不能用预处理的结果 O ( 1 ) O(1) O(1) 回答";若是"找一个满足条件的配对",就问"能不能用哈希/双指针"。

2.4 枚举"关键参数":把结构写出来

以例题 C(魔法阵)为例:题目要求找四个数 a < b < c < d a<b<c<d a<b<c<d 满足

b − a = 2 ( d − c ) b-a=2(d-c) b−a=2(d−c) 且 b − a < c − b 3 b-a<\dfrac{c-b}{3} b−a<3c−b。

  • 直接枚举四个变量是 O ( n 4 ) O(n^4) O(n4);
  • 观察到式子里的核心量是间隔 d − c d-c d−c。设 t = d − c t=d-c t=d−c,则 b − a = 2 t b-a=2t b−a=2t,且 c − b > 3 ⋅ 2 t = 6 t c-b>3\cdot2t=6t c−b>3⋅2t=6t;
  • 于是枚举 t t t ( O ( n ) O(n) O(n)),再枚举两个"成对的"位置,用前缀和/计数 把剩余部分 O ( 1 ) O(1) O(1) 算出来,整体做到 O ( n 2 ) O(n^2) O(n2) 级别。

这一类题的通用步骤:

  1. 把所有等式/不等式写成"参数"形式(谁等于谁的几倍);
  2. 找出出现次数最多的那个量作为枚举对象;
  3. 剩下的量用"它决定了一个区间/一类对象"来聚合,用前缀和或计数数组处理。

2.5 例题 0:费解的开关------"枚举自由变量 + 递推"

5 × 5 5\times5 5×5 的灯阵,按下一盏灯会连带翻转它上下左右相邻 的灯,问能否在 6 6 6 步内把所有灯变亮。

枚举对象的选择 :直接枚举 25 25 25 个位置"按/不按",共 2 25 ≈ 3.3 × 10 7 2^{25}\approx3.3\times10^7 225≈3.3×107 种,太大。但注意

第 1 行的按键方案一旦确定,第 2 行就被唯一确定------因为第 1 行里还关着的灯,只能靠它正下方的按钮来翻亮;同理第 3 行由第 2 行确定......直到第 5 行。

于是:

  • 枚举第 1 行的按键方案 (只有 2 5 = 32 2^5=32 25=32 种,这是"自由变量");
  • 其余各行被唯一确定(每一行只负责"修好"上一行);
  • 最后检查第 5 行是否也全亮(它已经没有"下一行"可以帮忙了)。

这就是"枚举 + 递推"的范式:把解空间里"真正自由的部分"枚举掉,其余部分用约束推出来。例题 H(机器人塔)是它的另一个形态(自由变量是最底下一行)。

2.6 减少枚举规模的三种思路

思路 做法 例子
只枚举一半 折半枚举(meet in the middle): 2 40 → 2 20 2^{40}\to2^{20} 240→220 大价值背包、四数之和
只枚举一边 排序后枚举一边,另一边二分/贪心 例题 I(红蓝牌)
只枚举小的一边 值域小就枚举值域, n n n 小就枚举 n n n 例题 A、D

第三章 枚举的优化

枚举的优化有两个方向:减少枚举对象的数量(剪枝)与降低每次检查的代价(预处理)。 前者让循环变少,后者让循环体变轻。

3.1 方向一:剪枝------让枚举对象本身变少

  1. 可行性剪枝 :枚举过程中发现不可能成为答案就提前退出。
    例:枚举子集求最大和时,若当前和已经是全局最大,剩余部分不用再枚举。
  2. 最优性剪枝:搜索类问题里用"当前解 + 乐观估计"与已有最优解比较(分支限界)。
  3. 上下界剪枝 :循环变量的取值范围由约束直接算出,而不是"从 1 到 n 都试一遍"。
    例:求 a + b = N a+b=N a+b=N 的解,枚举 a a a 时只枚举到 N / 2 N/2 N/2(利用对称性),并可直接算出 b = N − a b=N-a b=N−a,省掉一层循环。
  4. 对称性去重 : ( i , j ) (i,j) (i,j) 与 ( j , i ) (j,i) (j,i) 等价时强制 i < j i<j i<j。
  5. 利用约束方程消元 : a + b + c = N a+b+c=N a+b+c=N 时只需枚举 a , b a,b a,b, c c c 由方程算出------这是"降一层循环"最常用的手段。

经验:先写朴素暴力,然后盯着最内层循环问:"这个变量真的需要循环吗?它是不是被其他变量唯一确定了?"

3.2 方向二:预处理------让检查变 O ( 1 ) O(1) O(1)

预处理手段 适用 效果
一维前缀和 区间和 O ( n ) → O ( 1 ) O(n)\to O(1) O(n)→O(1)
二维前缀和 子矩阵和 O ( n m ) → O ( 1 ) O(nm)\to O(1) O(nm)→O(1)
差分 区间加、单点查 O ( n ) → O ( 1 ) O(n)\to O(1) O(n)→O(1) 每次修改
ST 表 / 线段树 区间最值 O ( n ) → O ( 1 ) O(n)\to O(1) O(n)→O(1)(静态,ST 表)
哈希表 / 桶 "是否存在配对"、"同余类计数" O ( n ) → O ( 1 ) O(n)\to O(1) O(n)→O(1)
排序 + 二分 "找第一个满足条件的" O ( n ) → O ( log ⁡ n ) O(n)\to O(\log n) O(n)→O(logn)
单调队列 / 双指针 窗口最值与窗口计数 内层 O ( n ) → O(n)\to O(n)→ 摊还 O ( 1 ) O(1) O(1)

核心用法:前缀和 + 计数数组 = 把"枚举两个端点"降成"枚举一个端点 + O ( 1 ) O(1) O(1) 查表"。

例题 G(入阵曲)是这条路的代表:求"子矩阵和是 K K K 的倍数"的个数。

  • 二维前缀和后仍需枚举 O ( n 2 m 2 ) O(n^2m^2) O(n2m2) 个矩形;
  • 但"和是 K K K 的倍数"    ⟺    \iff ⟺ 两个前缀和对 K K K 同余;
  • 于是固定上下边界后,只需扫一遍列,用桶统计每类余数出现了几次 ,答案加上 ( c 2 ) \binom{c}{2} (2c) 的累加即可。

复杂度 O ( n 2 m ) O(n^2m) O(n2m)(把 n , m n,m n,m 中较小的作为"行"枚举,可再优化为 O ( min ⁡ ( n , m ) 2 max ⁡ ( n , m ) ) O(\min(n,m)^2\max(n,m)) O(min(n,m)2max(n,m)))。

3.3 利用单调性把内层循环"摊还"掉

当出现下面的结构时,可以把内层循环从 O ( n ) O(n) O(n) 降到摊还 O ( 1 ) O(1) O(1):

枚举左端点 l l l 时,满足条件的右端点 r r r 随 l l l 增大而单调不减。

理由是 r r r 只会向右移动 n n n 次,总移动次数 O ( n ) O(n) O(n),所以双指针整体是 O ( n ) O(n) O(n)。

例题 F(统计子矩阵)就是这样:给定 K K K,统计元素和 ≤ K \le K ≤K 的子矩阵个数。

  • 元素非负 ⇒ \Rightarrow ⇒ 固定上边界 u u u、下边界 d d d 后,"列上的区间和"关于左右端点单调;
  • 于是用双指针扫一遍列, O ( m ) O(m) O(m) 统计;
  • 总复杂度 O ( n 2 m ) O(n^2m) O(n2m)( n , m ≤ 500 n,m\le500 n,m≤500 时约 1.25 × 10 8 1.25\times10^8 1.25×108 次轻量操作,注意常数)。

3.4 枚举的顺序也很重要

  • 先排序再枚举:能把"是否存在配对"变成"双指针"或"前缀和 + 二分",也是贪心的前提(例题 I);
  • 按值域枚举 vs 按下标枚举 :值域小(如 ≤ 10 3 \le10^3 ≤103、 ≤ 10 6 \le10^6 ≤106)优先按值域(例题 A、D);
  • 从大到小 or 从小到大:涉及"找第一个满足条件的"时,顺序决定了能否用单调性。

3.5 枚举 + 数据结构 = 大多数"正解"

把上面几节合起来,本手册所有例题几乎都是下面这条主线的一个实例:

复制代码
确定枚举对象 → 想清楚复杂度 → 用预处理/单调性/哈希把内层消掉 → 得到正解
题目 枚举对象 内层怎么消
A Lucky PIN 答案( 10 3 10^3 103) 贪心扫一遍 S S S
B 插入排序 被修改的那一个数 直接推导名次变化
C 魔法阵 间隔 t t t + 一个位置 前缀和/计数数组
E 杨辉三角形 位置参数(列号 k k k) 二分 n n n + 组合数剪枝
F 统计子矩阵 上下边界 双指针
G 入阵曲 上下边界 前缀和取模 + 桶
H 机器人塔 最底下一行的排布( 2 h 2^h 2h) 其余各层递推唯一确定
I 红蓝牌 一侧子集 另一侧排序取前 r r r 大
J 区间操作 被问到的左端点 前缀和 + 离线 + 复用
D 取模 (不枚举) 数论等价刻画

第四章 子集枚举

4.1 用二进制表示集合

n n n 个元素的集合有 2 n 2^n 2n 个子集。把第 i i i 个元素"选/不选"记为 1 / 0 1/0 1/0,则每个子集对应一个 n n n 位二进制数(位掩码):

复制代码
mask = 0b1011  -> 选中了第 0、1、3 号元素

基础操作(务必背熟):

目的 写法
判断第 i i i 位是否为 1 mask >> i & 1 或 mask & (1<<i)
把第 i i i 位设为 1 `mask
把第 i i i 位设为 0 mask &= ~(1<<i)
取反(在 n n n 位内) mask ^ ((1<<n)-1)
子集元素个数 __builtin_popcount(mask)
最低位的 1 mask & (-mask)
消掉最低位的 1 mask &= mask - 1

4.2 枚举所有子集: O ( 2 n ) O(2^n) O(2n)

cpp 复制代码
for (int mask = 0; mask < (1 << n); ++mask) {
    // 处理子集 mask
}

n ≤ 20 n\le20 n≤20 时 2 20 ≈ 10 6 2^{20}\approx10^6 220≈106,非常轻松; n ≤ 24 n\le24 n≤24 时约 1.7 × 10 7 1.7\times10^7 1.7×107,也可以; n ≥ 27 n\ge27 n≥27 就要换方法(折半枚举)。

4.3 枚举一个集合的所有子集: O ( 3 n ) O(3^n) O(3n)

在很多"分组/划分"类题目里,需要枚举 m a s k mask mask 的所有子集:

cpp 复制代码
for (int sub = mask; ; sub = (sub - 1) & mask) {
    // sub 取遍 mask 的所有子集(含 0 与 mask 本身)
    if (sub == 0) break;
}

这段循环的均摊复杂度是 O ( 3 n ) O(3^n) O(3n)(每个元素有三种状态:不在 m a s k mask mask 中 / 在 m a s k mask mask 但不在 s u b sub sub 中 / 在 s u b sub sub 中)。这是子集 DP(如"划分成若干组"、"最小斯坦纳树")的基础。

4.4 枚举固定大小的子集:Gosper's Hack

需要枚举"所有大小为 k k k 的子集"时,逐个 popcount 过滤太慢,可以用 Gosper's Hack 直接跳到下一个:

cpp 复制代码
// 枚举所有 popcount == k 的 n 位掩码(k>=1)
int mask = (1 << k) - 1;
while (mask < (1 << n)) {
    // 处理 mask
    int c = mask & -mask;
    int r = mask + c;
    mask = (((r ^ mask) >> 2) / c) | r;
}

4.5 折半枚举(Meet in the Middle)

当 n n n 到 40 40 40 左右、 2 n 2^n 2n 不可行时:把元素分成两半,各自枚举 2 n / 2 2^{n/2} 2n/2 个子集并存下它们的贡献 (排序/哈希),然后在两半之间配对。

典型: n = 40 n=40 n=40 的子集和问题------两半各 2 20 ≈ 10 6 2^{20}\approx10^6 220≈106,排序后双指针/二分配对,总复杂度 O ( 2 n / 2 log ⁡ 2 n / 2 ) O(2^{n/2}\log 2^{n/2}) O(2n/2log2n/2)。

4.6 补充例题 L:用子集枚举解决"选或不选"类问题

模型 1(枚举 + 判断) :有 n n n 个开关,每个开关控制若干盏灯,问是否存在方案让所有灯都灭。

👉 枚举 2 n 2^n 2n 个开关组合,逐位模拟, O ( 2 n ⋅ m ) O(2^n\cdot m) O(2n⋅m)。 n ≤ 20 n\le20 n≤20 可行。

模型 2(枚举 + 计数去重) :统计"由给定数字集合能拼出的不同数"的个数。

👉 枚举子集,把选出的数字排序后放进 set 去重。

模型 3(枚举 + 递推,呼应例题 0):状态空间巨大但"自由变量"很少时,只枚举自由变量(如费解的开关的第一行),其余部分递推。

模型 4(枚举 + 容斥) :求"至少/恰好"的计数时,枚举子集做容斥:

∣ ⋃ A i ∣ = ∑ ∅ ≠ S ( − 1 ) ∣ S ∣ + 1 ∣ ⋂ i ∈ S A i ∣ \Big|\bigcup A_i\Big|=\sum_{\emptyset\ne S}(-1)^{|S|+1}\Big|\bigcap_{i\in S}A_i\Big| ⋃Ai =∅=S∑(−1)∣S∣+1 i∈S⋂Ai

用 mask 枚举 S S S,每个 S S S 判断正负号(popcount 的奇偶)。


第五章 识别手册与易错点

5.1 看见这些,先想枚举(以及枚举什么)

题目信号 枚举对象 优化方向
"有多少种不同的......"且取值范围小 答案本身(值域枚举) 判断可行性用贪心/检查
"统计满足条件的子矩阵/子区间个数" 上下(左右)边界 二维前缀和 + 双指针/桶
"子数组和是 K K K 的倍数 / 满足同余" 前缀和的余数 桶计数
出现"四个数满足某些等式" 间隔/比例参数 前缀和 + 计数数组
n ≤ 20 n\le20 n≤20 子集(位掩码) O ( 2 n ) O(2^n) O(2n)、 O ( 3 n ) O(3^n) O(3n)、折半
n ≤ 40 n\le40 n≤40 折半后的子集 meet in the middle
有"每次修改后询问排名/相对大小" 只枚举被修改的元素 直接推导名次变化
有"取模"、"某个不变量" 取模后的状态 状态数有限 ⇒ \Rightarrow ⇒ 直接枚举状态
图形/规则生成的"塔、三角形、棋盘" 自由变量(其余由规则决定) 递推
两个数组配对求最优 排序后的配对位置 贪心(排序 + 依次配对)

5.2 复杂度速查:枚举几层能过?

循环层数 典型规模 能否通过
4 层 n ≤ 100 n\le100 n≤100 可以( 10 8 10^8 108 以内)
3 层 n ≤ 500 n\le500 n≤500 可以
2 层 n ≤ 5000 n\le5000 n≤5000 可以
2 层 + 内层 O ( 1 ) O(1) O(1) 查询 n , m ≤ 2 × 10 5 n,m\le2\times10^5 n,m≤2×105 可以(约 4 × 10 10 4\times10^{10} 4×1010 不行!需要 O ( n log ⁡ n ) O(n\log n) O(nlogn))
2 n 2^n 2n n ≤ 20 n\le20 n≤20(勉强 24) 可以
3 n 3^n 3n n ≤ 13 n\le13 n≤13 左右 可以

注意最后一行的陷阱: n = 2 × 10 5 n=2\times10^5 n=2×105 的双重 循环是 4 × 10 10 4\times10^{10} 4×1010,不行 !只有"一重枚举 + O ( 1 ) O(1) O(1) 查询"或"一重枚举 + 双指针"才行。

5.3 易错点清单

枚举范围类

  1. 上界写成 <n 但答案可能取到 n n n(长度、下标、数量都容易犯);
  2. 忘记空集/空操作/长度为 0 这些退化情况;
  3. 有重复元素时忘记去重(要"不同的值"却按"不同的下标"计数)。

重复计数类

  1. 无序对按有序枚举(应强制 i < j i<j i<j),或反之;
  2. 子集枚举时把"同一个集合的不同枚举顺序"算成多个方案;
  3. 容斥时符号写错( ∣ S ∣ |S| ∣S∣ 的奇偶)。

溢出类

  1. 计数答案超 int( ( 5000 2 ) ≈ 1.2 × 10 7 \binom{5000}{2}\approx1.2\times10^7 (25000)≈1.2×107 还在 int 内,但 ( 2 × 10 5 2 ) ≈ 2 × 10 10 \binom{2\times10^5}{2}\approx2\times10^{10} (22×105)≈2×1010 就超了);
  2. 乘法先加后乘要 1LL:(long long)a * b;
  3. 需要取模的题忘记取模,或取模后比较大小(比较不能取模!)。

优化类

  1. 双指针写错单调性方向(右指针回退会导致复杂度退化到 O ( n 2 ) O(n^2) O(n2));
  2. 前缀和下标从 0 还是 1 开始混用(pre[r]-pre[l-1] 与 pre[r+1]-pre[l]);
  3. 取模后的桶计数:余数范围是 0 , K − 1 0,K-1 0,K−1,数组要开 K K K 大小; K K K 很大时用哈希表;
  4. 位运算优先级:mask & 1 == 1 是错的(== 优先级高于 &),要写 (mask & 1) == 1;
  5. 1 << n 在 n ≥ 31 n\ge31 n≥31 时溢出(用 1LL << n)。

第六章 例题分析

每题统一结构:题意 → 枚举对象的选择 → 优化 → 复杂度 → 易错点 。

标 ★ 的是这道题的"灵光点"。



第八章 反例与对照:什么时候不该枚举

本章专门收录"看起来该枚举、其实不该"的题型------判断"不该枚举"和"会枚举"同样重要。

8.1 反例一:P8807 取模(例题 D)

  • 假象 :题目问"是否存在两个不同的 x , y ≤ m x,y\le m x,y≤m 使 n   m o d   x = n   m o d   y n\bmod x=n\bmod y nmodx=nmody",第一反应是"枚举 x x x 算余数、用桶判重", O ( m ) O(m) O(m)。
  • 现实 : m ≤ 10 9 m\le10^9 m≤109,且 T ≤ 10 5 T\le10^5 T≤105,枚举必爆。
  • 正解 : O ( 1 ) O(1) O(1) 判定------无解    ⟺    lcm ⁡ ( 1.. m ) ∣ ( n + 1 ) \iff\operatorname{lcm}(1..m)\mid(n+1) ⟺lcm(1..m)∣(n+1) ,且 m ≥ 23 m\ge23 m≥23 时必然有解。
  • 教训 :枚举的前提是"对象规模可承受" 。规模是 10 9 10^9 109 时,要找等价刻画、有限状态(抽屉原理)或阈值,而不是继续优化枚举。

8.2 反例二:P11475 红蓝牌(例题 I)

  • 假象 :"选若干红牌和若干蓝牌" ⟹ 枚举两侧子集, 2 N + M 2^{N+M} 2N+M。
  • 现实 : N , M ≤ 21 N,M\le21 N,M≤21 ⟹ 2 42 2^{42} 242 不可行。
  • 正解 :固定一侧后另一侧的最优选择可以直接算 (贡献可分离 ⟹ 排序取前 r r r 大),于是只枚举一侧: 2 min ⁡ ( N , M ) = 2 21 2^{\min(N,M)}=2^{21} 2min(N,M)=221。
  • 教训 :"两侧都枚举"几乎总是可以砍成"枚举一侧 + 另一侧贪心/查询"。

8.3 反例三:P10401 区间操作(例题 J)

  • 假象 :"所有区间和的异或" ⟹ 按二进制位拆开统计( ⊕ \oplus ⊕ 常与按位思路配套)。
  • 现实 :按位拆解是错的 ------区间和 S k − S l − 1 S_k-S_{l-1} Sk−Sl−1 的第 b b b 位受低位借位影响,不能独立统计。反例: a a a 全为 1 时 ( 1 , 4 ) (1,4) (1,4) 与 ( 2 , 5 ) (2,5) (2,5) 的项完全相同(真值都是 4),按位思路给出 4 和 0。
  • 正解 :前缀和 + 离线只枚举被问到的左端点 , O ( n 2 + q log ⁡ q ) O(n^2+q\log q) O(n2+qlogq)。
  • 教训 :"看起来能按位独立"必须验证(自己造小数据对拍),否则会被"漂亮的假证"骗到。

8.4 反例四:什么时候"枚举"就是最优解

情形 为什么枚举是对的
答案值域很小(例题 A: 10 3 10^3 103 种) 枚举答案 + O ( N ) O(N) O(N) 判定,常数极小、代码极短
子集规模 n ≤ 20 n\le20 n≤20(例题 L) 2 n 2^n 2n 只有 10 6 10^6 106,比任何"聪明算法"都稳
需要"枚举 + 递推"(例题 0、H) 自由变量少,其余由约束唯一确定
数据范围小( n ≤ 100 n\le100 n≤100)且时限宽松 写暴力最省时间,把时间留给其他题

8.5 一张决策表:枚举 / 换对象 / 不枚举

复制代码
数据范围先算一遍
├─ 暴力复杂度 × 最坏数据 ≤ 10^8?
│   ├─ 是 → 直接写暴力(注意不重不漏),别浪费时间想优化
│   └─ 否 → 继续
├─ 答案/状态的取值范围很小(≤10^6)?
│   ├─ 是 → 换枚举对象:枚举答案/状态 + O(1)~O(N) 判定(例题 A、D)
│   └─ 否 → 继续
├─ 内层循环在做"求和/求最值/找配对"?
│   ├─ 是 → 前缀和 / ST 表 / 哈希 / 双指针 / 二分(例题 C、F、G、J)
│   └─ 否 → 继续
├─ 枚举对象是"两侧各选一些"?
│   ├─ 是 → 只枚举一侧 + 另一侧贪心/查询(例题 I)
│   └─ 否 → 继续
└─ 规模是 10^9 级的连续量(不是组合对象)?
    └─ 是 → 这题多半不是枚举题:找等价刻画 / 数学性质 / 数据结构(例题 D)

第七章 训练建议

7.1 训练顺序

阶段 题目 目标
1 A Lucky PIN、B 插入排序 学会"换枚举对象"和"只枚举被改动的部分"
2 0 费解的开关、H 机器人塔 学会"枚举自由变量 + 递推"
3 L 子集枚举专题 熟练位运算与容斥
4 G 入阵曲、F 统计子矩阵 学会"前缀和 + 桶/双指针"
5 C 魔法阵 学会"枚举关键参数 + 前缀和"
6 E 杨辉三角形、I 红蓝牌、J 区间操作 综合运用

7.2 训练方法

  1. 先写暴力,再优化 :每道题都先写出"不重不漏"的 O ( 爆 ) O(\text{爆}) O(爆) 暴力,并用它验证优化后的代码(对拍 1000 组随机数据)。
  2. 写复杂度预算 :动手前在纸上写"我的枚举是 O ( ? ) O(?) O(?), n n n 最大是 ? ? ?,乘积是 ? ? ?",超了就换对象。
  3. 背模板:二维前缀和、双指针、桶计数、位运算子集枚举------这四段代码要能默写。
  4. 整理"换对象"清单:把自己做过的题按"枚举对象"分类(枚举答案/枚举参数/枚举边界/枚举子集/枚举配对),考试时按清单搜索。

7.3 下一步

  • 搜索:枚举 + DFS/BFS + 剪枝(迭代加深、A*、IDA*);
  • 状压 DP :子集枚举的进阶( O ( 3 n ) O(3^n) O(3n)、 O ( 2 n ⋅ n ) O(2^n\cdot n) O(2n⋅n)、轮廓线 DP);
  • 折半枚举 与随机化枚举(随机打乱 + 贪心多次);
  • 枚举 + 数据结构:把本手册的"桶/前缀和"换成线段树、树状数组、平衡树。
相关推荐
小O的算法实验室1 小时前
IEEE TAI,RoCo:基于角色的LLM协作实现自动启发式设计
算法
小范的技术工坊1 小时前
大模型蒸馏
人工智能·算法·数据挖掘·大模型
Benny_Tang1 小时前
P9527 [JOIST 2022] 洒水器 / Sprinkler 题解
c++·算法
kiracrimson2 小时前
希尔排序:让插入排序不再怕逆序
数据结构·算法·排序算法
M78佐菲2 小时前
ARM学习笔记(12)
linux·arm开发·笔记·嵌入式硬件·学习
奇思妙想聪明勤奋的小羊2 小时前
ai-agent-book第五章:Coding Agent 与通用 Agent 学习笔记
人工智能·笔记·学习
垆边人似月.2 小时前
华为机考题:最小代价爬楼梯
c语言·c++
可涵不会debug2 小时前
C++ std::bind 实战教程:线程池、回调场景全覆盖
linux·开发语言·c++
优化Henry2 小时前
BBU与RRU光链路详解:从双纤一收一发到单纤双向
网络·笔记·学习·5g·tdd