暴力不是“随便套几层循环”。它有明确的前提:方案总数 × 每个方案的代价 ≤ 10⁸ 左右。写代码之前先把这两个数算出来——算得出来,你就知道该不该动手;算不出来,说明你还没想清楚要枚举什么。这一节练的就是这个动作,外加两个把方案数变小的工具:换枚举对象、只枚举一半。
把 1, 2, …, 9 这 9 个数字分成 3 组,每组 3 个,分别拼成 3 个三位数,要求这 3 个三位数之比为 1 : 2 : 3。求所有方案。
先别写代码,先数一下:这道题到底有多少种可能?同样一道题,三种数法给出三种命运。
| 枚举什么 | 方案数 | 每个方案要做什么 | 总操作数 |
|---|---|---|---|
| a、b、c 各自从 100 试到 999 | 900³ = 7.3×10⁸ | 拆 9 位查重 | ≈ 6×10⁹,必然 TLE |
| 1~9 的全排列 | 9! = 362 880 | 切 3 段 + 查重 | ≈ 3×10⁶,能过 |
| 只枚举 a,令 b = 2a、c = 3a | 207 | 拆 9 位查重 | ≈ 2×10³,眨眼 |
方案数 ≤ 10⁷ 且每个方案 O(1):直接写,不用想优化。
值域小(≤ 10⁷):能开数组,就先用数组把信息存下来,再扫一遍。
答案唯一且可以“找到就退出”:平均代价远小于最坏代价,也敢写。
反面信号:n ≥ 25 还想枚举子集、n ≥ 12 还想枚举排列、n ≥ 5000 还写 O(n²)。见到这三个数,先停下来算。
这一节的三个工具分别负责“数得更少”“表示得更紧”“生成得更快”。
| 组成 | 含义 / 格式要求 |
|---|---|
x | 待统计的整数。按无符号位模式解释,所以传负数会得到它的补码里 1 的个数。 |
返回值 | x 的二进制表示中 1 的个数,范围 0..32。 |
复杂度 | 编译成一条 popcnt 指令或常数级查表,O(1)。比手写循环快一个量级。 |
64 位版本 | __builtin_popcountll(x)。对 long long 用 32 位版本只会统计低 32 位。 |
典型用途 | 二进制枚举里筛“恰好选 k 个”的子集、算汉明距离、判 2 的幂(popcount == 1)。 |
for (int mask = 0; mask < 8; ++mask)
cout << mask << " -> " << __builtin_popcount(mask) << "\n";
// 0->0 1->1 2->1 3->2 4->1 5->2 6->2 7->3名字以两个下划线开头,是 GCC 内建函数,不是标准库函数;比赛用的编译器都有。
它只吃整数,不吃 bitset<N>(那个用 .count())。
| 组成 | 含义 / 格式要求 |
|---|---|
(mask >> i) & 1 | 取第 i 位。位编号从 0 开始(最低位)。结果只能是 0 或 1,可直接当条件。 |
mask | (1 << i) | 把第 i 位置 1。按位或不动其它位。 |
mask & ~(1 << i) | 把第 i 位清 0。~ 之后只有第 i 位是 0,其余全 1。 |
mask ^ (1 << i) | 翻转第 i 位:0 变 1,1 变 0。 |
1 << n | 2ⁿ。n ≥ 31 时 int 装不下(1<<31 是负数),要写 1LL << n。 |
mask & (mask - 1) | 清掉最低位的 1。配合 while 可以只扫 mask 里为 1 的那些位。 |
int mask = 0b1011; // 1011 = 11:第 0、1、3 位是 1
cout << ((mask >> 1) & 1) << "\n"; // 1:取第 1 位
mask |= (1 << 2); // 置位 → 1111
mask &= ~(1 << 0); // 清位 → 1110
cout << __builtin_popcount(mask) << "\n"; // 3位编号从 0 开始:第 0 位的权值是 1,第 i 位的权值是 2ⁱ。
& 和 && 不是一回事:mask && 3 是逻辑与,结果是 0/1,写成它就会变成“全选”或“全不选”。
循环上界写成 1 << 31 会让条件永真,直接死循环 → TLE。
| 组成 | 含义 / 格式要求 |
|---|---|
first, last | 要重排的区间,左闭右开。 |
前提 | 区间当前必须升序,否则只能拿到“从当前排列往后”的那部分,前面的全丢。函数不会帮你排序。 |
返回值 | 还有下一个排列 → 返回 true 并原地改好;已经是最后一个排列 → 返回 false,同时把区间重置为升序(即最小的那个排列)。 |
个数 | n 个互不相同的元素共有 n! 个排列,do-while 正好执行 n! 次。 |
写法 | 必须 do-while,不能 while:第一个排列也要输出。 |
int a[] = {1, 2, 3}; // 升序 = 字典序最小的排列
do {
for (int i = 0; i < 3; ++i) cout << a[i] << " ";
cout << "\n";
} while (next_permutation(a, a + 3)); // 还有下一个就继续
// 输出 6 行:1 2 3 / 1 3 2 / 2 1 3 / 2 3 1 / 3 1 2 / 3 2 1元素重复时按“不重复的排列”输出,总数少于 n!。
求上一个排列用 prev_permutation,初值要写成降序。
上面为了短,每行末尾多打了一个空格;真要提交时改成 if (i) cout << " "; 的写法。
最省事的写法是三层的循环 a, b, c 各 100..999,再拆位检查——7.3×10⁸ 个三元组,每个还要拆 9 位,总计 6×10⁹ 次操作,1 秒的题必死。
关键观察:题目给了比例。只要 a 定了,b = 2a、c = 3a 就定了。枚举对象从“三个数”换成“一个数”,方案数从 10⁹ 掉到 10²。这就是“数清楚”的第一层收益:不用算法,只换视角。
a 的范围也不是 100..999:a 自己得是三位数、不含 0、数字互不相同,最小就是 123;c = 3a 也得是三位数(3a ≤ 987),所以 a ≤ 329。能算出边界就不要硬写 999。
剩下的活是判重:三个三位数一共 9 个数字,要求恰好是 1..9 各一次。用 cnt[10] 数每个数字出现的次数,出现 0 或某个数字第二次出现就否决,最后检查 1..9 是否都恰好一次。
#include <bits/stdc++.h>
using namespace std;
int main() {
for (int a = 123; a <= 329; ++a) { // 3a ≤ 987 ⇒ a ≤ 329
int cnt[10] = {0}; // cnt[d]:数字 d 出现了几次
int p[3] = {a, 2 * a, 3 * a}; // 由比例生成,不用三层循环
bool ok = true;
for (int t = 0; t < 3 && ok; ++t) {
int x = p[t]; // 拆 x 的三位,x 是副本,不动 p[t]
for (int j = 0; j < 3; ++j) {
int d = x % 10;
x /= 10;
if (d == 0 || ++cnt[d] > 1) { // 出现 0 或重复,立刻否决
ok = false;
break;
}
}
}
if (!ok) continue;
bool all = true; // 1~9 是否每个恰好出现一次
for (int d = 1; d <= 9; ++d)
if (cnt[d] != 1) { all = false; break; }
if (all) cout << a << " " << 2 * a << " " << 3 * a << "\n";
}
return 0;
}| 代码片段 | 含义与理由 |
|---|---|
a <= 329 | 3a 必须还是三位数,987 / 3 = 329;下界 123 保证 a 本身是三位数。区间 [123, 329] 共 207 个数,这就是全部方案数。 |
int cnt[10] = {0} | 数字只可能是 0..9,用值当下标,10 个格子够。初始化写成 {0} 是关键,否则数组里是随机值。 |
int p[3] = {a, 2 * a, 3 * a} | 由比例直接算出另外两个数。这一步把三层循环压成一层,是整个解法的核心。 |
int d = x % 10; x /= 10; | 取末位、砍掉末位,三次拆完一个三位数。x 是副本,所以循环结束后 p[t] 不变。 |
d == 0 || ++cnt[d] > 1 | 左边先短路:出现 0 直接否决,不碰 cnt[0]。右边先自增再比较:第一次出现得 1 不触发,第二次得 2 触发。一句代码同时完成“范围检查”和“判重”。 |
cnt[d] != 1 | 9 个数字两两不同且都在 1..9 的充要条件:每个数字恰好出现一次。 |
cout << a << " " << 2 * a ... | a 从小到大枚举,输出天然按第一个数升序,不需要额外排序。 |
n 个元素“选或不选”,一共 2ⁿ 种结果。把每种结果编码成一个整数:第 i 位是 1 表示选第 i 个元素。于是 for (int mask = 0; mask < (1 << n); ++mask) 这一层循环就把所有子集走完了。
它比 n 层嵌套循环短得多,也比递归早 7 节就能用(第 21 节才讲递归)。代价是不能剪枝:2ⁿ 个方案一个不落。
// 演示:n ≤ 20,统计元素和恰好等于 S 的子集个数(不含空集)
int n = 5, S = 10;
int a[5] = {1, 2, 3, 4, 5};
int ans = 0;
for (int mask = 0; mask < (1 << n); ++mask) { // 2⁵ = 32 个 mask,一个 mask 一个子集
int sum = 0;
for (int i = 0; i < n; ++i)
if ((mask >> i) & 1) sum += a[i]; // 第 i 位是 1 ⇒ 选 a[i]
if (mask != 0 && sum == S) ++ans; // 空集的和是 0,本题不要
}
cout << ans << "\n"; // 3| 代码片段 | 含义与理由 |
|---|---|
1 << n | 左移 n 位就是 2ⁿ。n = 20 时是 1048576,mask 从 0 到 2ⁿ−1,正好一个子集一个编号。 |
mask < (1 << n) | 上界写 1 << n 且不带等号。写成 <= 会多枚举一个不存在的子集。 |
(mask >> i) & 1 | 把第 i 位移到最低位再与 1 相与,结果是 0 或 1,可以直接当条件用。 |
sum += a[i] | 这一位是 1,说明子集包含 a[i]。注意位编号对应的是下标 i,不是 a[i] 的值。 |
mask != 0 | mask = 0 表示一个都不选。很多题不允许空集,这一句必须写。 |
| n | 2ⁿ | 内层 O(n) 时的总操作数 | 结论 |
|---|---|---|---|
| 10 | 1 024 | ≈ 10⁴ | 随便过 |
| 16 | 65 536 | ≈ 10⁶ | 过 |
| 20 | 1 048 576 | ≈ 2×10⁷ | 过 |
| 25 | 33 554 432 | ≈ 8×10⁸ | 超时,改折半 |
| 30 | 1 073 741 824 | ≈ 3×10¹⁰ | 不可能 |
把这张表记住比记住代码有用:看到 n ≤ 20 就想二进制枚举,看到 n ≥ 25 就换方法。如果题目还要求“恰好选 k 个”,用 __builtin_popcount(mask) == k 筛掉大部分方案——C(n, k) / 2ⁿ 的比例可以从 C(20, 10) / 2²⁰ ≈ 18% 手算出来。
n = 40 时 2⁴⁰ ≈ 1.1×10¹²,一秒跑不完。但把数组切成两半,每半 20 个,各枚举 2²⁰ ≈ 1.05×10⁶ 个子集和;再让“左半取 s、右半取 S − s”配对就行。
配对靠排序 + 二分,正好用上第 13 节的 lower_bound 与 upper_bound。这就是折半搜索(meet in the middle):把一次 2ⁿ 的枚举,换成两次 2^(n/2) 的枚举加一次二分。
int h = n / 2; // 切成 20 + 20
vector<long long> L, R;
for (int mask = 0; mask < (1 << h); ++mask) { // 左半:2²⁰ 个子集和
long long s = 0;
for (int i = 0; i < h; ++i)
if ((mask >> i) & 1) s += a[i];
L.push_back(s);
}
for (int mask = 0; mask < (1 << (n - h)); ++mask) { // 右半:2²⁰ 个子集和
long long s = 0;
for (int i = 0; i < n - h; ++i)
if ((mask >> i) & 1) s += a[h + i]; // 原数组下标是 h + i
R.push_back(s);
}
sort(R.begin(), R.end()); // 只排一半
long long ans = 0;
for (long long s : L) // 左半每个和,去右半里数 S−s 出现几次
ans += upper_bound(R.begin(), R.end(), S - s)
- lower_bound(R.begin(), R.end(), S - s);| 代码片段 | 含义与理由 |
|---|---|
int h = n / 2 | 把 40 个数切成 20 + 20。两半不必等长,差 1 无所谓。 |
1 << h | 2²⁰ ≈ 1.05×10⁶,比 2⁴⁰ 小了六个数量级。折半的全部收益就在这个指数上。 |
a[h + i] | 右半第 i 个元素在原数组里是 a[h+i]。下标错位是折半搜索最常见的 WA。 |
sort(R.begin(), R.end()) | 只排右半。左半保持无序,靠二分去查。 |
upper_bound - lower_bound | 相减 = 等于 S−s 的元素个数,也就是能和 s 配成 S 的方案数。 |
| n | 直接枚举 2ⁿ | 折半后的枚举量 | 排序 + 二分 | 总量级 |
|---|---|---|---|---|
| 20 | 1.0×10⁶ | 2.1×10³ | ≈ 2×10⁴ | 10⁴ |
| 30 | 1.07×10⁹ | 6.6×10⁴ | ≈ 1×10⁶ | 10⁶ |
| 40 | 1.1×10¹² | 2.1×10⁶ | ≈ 4×10⁷ | 10⁷ |
代价是内存:两个数组各存约 10⁶ 个 long long,各 8 MB,还扛得住。再大就得换个思路。记住这个手势:指数太大就砍一半——枚举一半、排序一半、另一半去查。
循环上界写 1 << 31:它是 int 的负数(−2147483648),条件永真 → 死循环 TLE。n ≥ 31 时必须写 1LL << n,循环变量也要 long long。
next_permutation 之前忘了排序:初值是 {3, 2, 1} 时函数第一次就返回 false,6 个排列只输出 1 个 → WA。
用 while 代替 do-while:第一个排列被跳过。n = 1 时程序一行都不输出 → WA(样例就挂)。
__builtin_popcount 用错宽度:对 long long 用它只统计低 32 位,1LL << 40 会得到 0 → WA。64 位要用 __builtin_popcountll。
把“第 i 个元素”和“值 i”搞混:写 if (mask & (1 << a[i])) 是把 a[i] 当位号,值一过 30 就移出有效位,答案全错 → WA。位号永远是下标。
把 2ⁿ 个方案的结果全存下来:n = 25 时是 3.3×10⁷ 个 int,光数组就要 130 MB → MLE,很多平台紧接着就是 RE。边枚举边统计,别存。
没估量级就开写:n = 30 直接枚举子集,2³⁰ ≈ 1.07×10⁹ 个方案,每个还要 O(n),实际 3×10¹⁰ 次操作 → 必然 TLE。先数一遍就不会有这一条。
关掉页面,把 P1008 的代码敲一遍并提交,确认输出正好 4 行。
先把输出换成计数器,打印候选数应是 207;再把 a <= 329 改成 a <= 987,候选数变成 865,答案仍是那 4 行——说明缩小枚举范围不改变正确性,只改变耗时。
改一改:用 next_permutation 枚举 1~9 的全排列(9! = 362 880 种),每 3 位切成一段验证 1 : 2 : 3,用 clock() 量一下两种写法的耗时。你会看到 207 次和 3.6×10⁵ 次的差距——这就是“数清楚”的收益。
评测机 1 秒大约做 10⁸ 次简单操作(比较、加法、赋值)。所以判断能不能暴力,只需要算一个乘积:方案数 × 每个方案的代价,和 10⁸ 比大小。这一步在纸上做,比在评测机上做便宜得多。
| n | 2ⁿ(子集) | n!(排列) | 能不能暴力 |
|---|---|---|---|
| 10 | 1 024 | 3 628 800 | 都行 |
| 15 | 32 768 | 1.3×10¹² | 子集行,排列不行 |
| 20 | 1 048 576 | 2.4×10¹⁸ | 子集行(还要看每方案代价) |
| 25 | 33 554 432 | — | 勉强,2ⁿ·n 已到 8×10⁸ |
| 30 | 1 073 741 824 | — | 不行 |
折半的账:2ⁿ = 2^(n/2) × 2^(n/2)。两半各枚举 2^(n/2),再花 O(2^(n/2) · n/2) 排序、O(2^(n/2) · n/2) 配对,总量从 2ⁿ 降到约 n·2^(n/2)。n = 40 时:1.1×10¹² → 4×10⁷,快约 2.7 万倍。指数上的 n 变成 n/2,比任何常数优化都值钱。
组合恒等式:对固定的 n,C(n, 0) + C(n, 1) + … + C(n, n) = 2ⁿ。也就是说“按子集大小分类枚举”和二进制枚举覆盖的是同一批对象,只是访问顺序不同。二进制枚举省掉了 k 层循环的写法成本,代价是不能提前剪枝——这正是第 23 节 DFS 要补上的东西。
排列的增长:10! = 3.6×10⁶,11! = 4.0×10⁷,12! = 4.8×10⁸,13! = 6.2×10⁹。所以 next_permutation 全排列的实用上限大约在 n = 11;n = 12 就算每轮只做 O(1) 也已经踩线。
按字典序输出 1 到 n 的所有不重复排列,即 n 的全排列,同一个序列中不允许出现重复的数字。
输入格式:一个整数 n(1 ≤ n ≤ 9)。
输出格式:共 n! 行,每行一个由 1~n 组成的序列,每个数字保留 5 个场宽。
n = 9 时共有 9! = 362 880 行。递归生成当然可以,但标准库里已经有一个“按字典序吐出下一个排列”的函数。
这个函数要求区间一开始是升序,否则你只能从当前排列往后走,前面的全丢了。
场宽用 setw(5)。它只对紧随其后的那一个输出生效,所以要写在循环里。
把数组 a 初始化成 1, 2, …, n——这是字典序最小的排列。
用 do-while 反复调用 next_permutation:它把 a 改成字典序的下一个排列,最后一个排列的下一次调用返回 false。
每轮输出 a 的 n 个数,每个用 setw(5) 补齐到 5 个字符,行末只换行。
为什么输出天然是字典序:next_permutation 的定义就是“下一个字典序”,从最小排列出发一路调用,顺序必然正确。
复杂度 O(n · n!):n = 9 时约 3.3×10⁶ 次输出,远在 10⁸ 之内。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; ++i) a[i] = i + 1; // 字典序最小的排列
do {
for (int i = 0; i < n; ++i)
cout << setw(5) << a[i]; // 每个数字占 5 个场宽
cout << "\n";
} while (next_permutation(a.begin(), a.end())); // 还有下一个就继续
return 0;
}| 片段 | 作用 |
|---|---|
a[i] = i + 1 | 初始化为 1..n 的升序,即字典序最小的排列。不从最小处出发,前面的排列就全都拿不到。 |
do { | 先输出再判断。写成 while 会跳过第一个排列;n = 1 时程序将一行都不输出。 |
setw(5) | 设置“下一次输出”的最小宽度为 5,右对齐、左边补空格。它只作用于紧跟其后的一个值。 |
next_permutation(a.begin(), a.end()) | 把区间改成下一个字典序排列,还有下一个则返回 true;已经是最后一个时返回 false,并把区间重置为升序。 |
cout << "\n" | 每行末尾只换行。场宽已经补齐,不要再手动加空格,否则行末多出尾随空格。 |
cout << a[i] << ' ' 代替场宽,格式不符直接 WA。