题单 · 习题解析

洛谷 P2114 [NOI2014] 起床困难综合症

★★★ 全部立论压在一句话上:**AND / OR / XOR 都是逐位的,输出的第 b 位只由输入的第 b 位决定** ⇒ 「在 10⁹ 个 x 里挑一个」拆成 30 个互不相干的小问题,而**两趟 O(n)**(把 `x = 0` 和 `x = 全1` 各推一遍)就能把 30 位一次问完;★★ 这条立论要单独验(正解和四个错法全压在它上面 ⇒ 互相对拍验的是零)—— 20 万组预测 vs 真跑 0 组不同,而这个 0 配了自检(把一扇门换成加法,当场对不上 15 万组);★★★ 十六格「触发 ≡ 抓获」全中,其中最深的一个错法是「只扫到 `m` 的最高位」——**`x ≤ m` 限制的是输入的位,不是输出的位**(一扇 `OR 1073741823` 就能顶到第 29 位,而那些位填 0 也能拿到、不花预算);★★★ 而这一页带回一条新的:**两个完全不同的 bug 可以共用同一条触发线** —— 「预算没累计」和「从低位往高位贪」被抓的轮次**一模一样**(32 轮交集 32),而 32 轮里它俩给的错误答案**两两不同**;⚠⚠ 「预算宽裕」那个对照档是被实测打回来重做的(`f(全1)` 的高位只有被 AND 清掉才会消失);⚠ 官方唯一那组样例把四个错法**全放过**,原因是同一句话:**它的答案只有一个二进制位**

⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P2114,日期见页头。两边不一致时信原站。

题目描述

21 世纪,许多人得了一种奇怪的病:起床困难综合症……(背景略)drd 的防御战线由 n 扇防御门组成。 每扇防御门包括一个运算 op 和一个参数 t,其中运算一定是 ORXORAND 中的一种, 参数则一定为非负整数。如果还未通过防御门时攻击力为 x,则其通过这扇防御门后攻击力将变为 x op t。 最终 drd 受到的伤害为对方初始攻击力 x 依次经过所有 n 扇防御门后转变得到的攻击力。

由于 atm 水平有限,他的初始攻击力只能为 0m 之间的一个整数 (即他的初始攻击力只能在 0, 1, …, m 中任选,但在通过防御门之后的攻击力不受 m 的限制)。 为了节省体力,他希望通过选择合适的初始攻击力使得他的攻击能让 drd 受到最大的伤害。

输入格式

第 1 行包含 2 个整数,依次为 n, m。接下来 n 行,每行一个字符串 op 和一个非负整数 t

输出格式

一行一个整数,表示 atm 的一次攻击最多使 drd 受到多少伤害。

数据规模与约定

测试点 n m 性质
1 100 0
2、3 1000 1000
4 10⁵ 10⁵ A
5 10⁵ 10⁵ B
6 10⁵ 10⁵
7 10⁵ 10⁹ B
8、9、10 10⁵ 10⁹
  • 特殊性质 A:存在一扇防御门为 AND 0
  • 特殊性质 B:所有防御门的操作均相同。

对于所有数据,保证 2 ≤ n ≤ 10⁵0 ≤ m ≤ 10⁹0 ≤ t ≤ 10⁹

时限 1 秒,内存 128000 KB(125 MB)。

输入输出样例

输入

3 10
AND 5
OR 6
XOR 7

输出

1

初始攻击力可选 0 ~ 10。取 4 时:4 AND 5 = 44 OR 6 = 66 XOR 7 = 1。 题面算过:取 0/2/4/6/8/10 最终都是 1,取 1/3/5/7/9 都是 0 ⇒ 最大伤害 1。 ⚠ 记住这组 —— 第 ⑦ 步会看到它把四个错法全放过了

1★★★ 关键的一句话:AND / OR / XOR 都是「逐位」的运算

★★★ 输出的第 b 位,只由输入的第 b 位决定 —— 一个位都不串

这三个运算的真值表是一位一位算的:(x op t) 的第 b 位 = x 的第 bop t 的第 b 位。 ⇒ 连着走 n 扇门也是一样 —— b 位从头到尾都待在第 b 位上,谁也不影响谁。

★ 这一句话把这道题从「在 10⁹x 里挑一个」变成 30 个互不相干的小问题: 「第 b 位填 0 还是填 1」。

★★ 而这 30 个小问题只要两趟就能全部问完: 把 x = 0x = 全 1 各推过一遍所有门,得到 a = f(0)b = f(全1)。 因为逐位独立,a 的第 b 位就是「这一位填 0 的结果」,b 的第 b 位就是「填 1 的结果」。 ⇒ O(n) 两趟,不是 30 趟。

⇒ 这就是第 46 章最后那句话的兑现: 位运算真正的价值不是快,是「一个整数就是一排开关」 —— 而这道题里, 那排开关彼此没有一根线连着,所以可以一根一根单独拨。

⚠ 这条立论要单独验 —— 正解和四个错法全都压在它上面

对拍验的是「两份实现算得一样」,验不了「我引用的那条性质本身成立吗」第 37 章 P2085 那条:参照物和解法共享一个假设,那个对拍验的是零)。

p2114Count.cpp 拿「真把 x 推一遍」当参照物,和「用 f(0) / f(全1) 逐位拼出来的预测值」比: 20 万组随机门序列 + 随机 x,0 组不同。

⚠ 而这个 0 配了自检:把最后一扇门换成加法(加法不是逐位运算,有进位)—— 同一段代码当场对不上 150 277 组。

2第 ① 版:把 x = 0..m 全试一遍 —— 而题面的测试点表直接告诉你它值 30 分

★ 那张测试点表就是出题人递过来的分数
测试点 n m 暴力跑得完吗
1 100 0 ★ 只有一个 x 可选
2、3 1000 1000 ★ 10⁶ 次,秒过
4~10 10⁵ 10⁵ ~ 10⁹ ✗ 最大 10¹⁴ 次

暴力稳拿 30 分,而且它是考场上五分钟就能写完的第一反应。 「出题人把暴力那一档也写好了」的又一次。

★ 顶格两把尺子:正解 2n + 30 = 200030 次基本操作(实测 0.43 毫秒), 暴力 n·(m+1) = 10¹⁴ 次 ⇒ 差 5 亿倍。这次「肯定超时」是真的。

p2114Brute.cpp第 ① 版:枚举所有 x(30 分)
// P2114 ★ 第 ① 版:把 x = 0 .. m 全试一遍 —— 答案永远对,它只挂在时间上
//
// ★ 复杂度 O(nm),顶格 10⁵ × 10⁹ = 10¹⁴ ⇒ 这次「肯定超时」是真的。
// ★★ 而题面的**测试点表**把它能拿多少分写得清清楚楚:
// 测试点 1(n = 100, m = 0)/ 2、3(n = 1000, m = 1000)⇒ **稳拿 30 分**。
// ⇒ [「出题人把暴力那一档也写好了」](/sol/p1776/)的又一次。
#include <bits/stdc++.h>
using namespace std;
int main() {
int n; long long m;
if (scanf("%d %lld", &n, &m) != 2) return 0;
vector<char> op(n); vector<unsigned> t(n);
for (int i = 0; i < n; i++) {
char s[8]; unsigned v;
if (scanf("%7s %u", s, &v) != 2) return 0;
op[i] = s[0]; t[i] = v;
}
long long best = 0;
for (long long x = 0; x <= m; x++) {
unsigned cur = (unsigned)x;
for (int i = 0; i < n; i++) {
if (op[i] == 'A') cur &= t[i];
else if (op[i] == 'O') cur |= t[i];
else cur ^= t[i];
}
best = max(best, (long long)cur);
}
printf("%lld\n", best);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

3★ 正解:从高位往低位,一位一位地贪

★ 贪心为什么对:高位的一个 1,比它下面所有位加起来还大

2ᵇ > 2ᵇ⁻¹ + … + 2 + 1。所以只要高位能拿到 1,不管下面损失多少都值 ⇒ 从高位往低位扫。

每一位只有三种情形:

f(0) 这一位 f(全1) 这一位 怎么办
1 任意 白拿 —— 这一位的 x 填 0 就有 1,一分钱预算都不花
0 1 要花 2ᵇ 的预算:used + 2ᵇ ≤ m 才拿
0 0 拿不到,跳过

⚠ 第二行那个判断必须是累计used + 2ᵇ ≤ m —— 预算是一次性的, 买好几位花的是它们的(见第 ⑤ 步)。 ⚠ 而第一行「白拿」的位绝对不能计入 used:那一位的 x 填的是 0,它对 x 的大小毫无贡献(见第 ⑥ 步)。

★ 位宽这笔账:t, m ≤ 10⁹ < 2³⁰ ⇒ 从 0 出发只做 AND/OR/XOR,结果永远 < 2³⁰第 0~29 位就够,int 装得下,这道题不需要 long long

p2114.cpp★ 正解:两趟 O(n) + 30 位贪心
// P2114 [NOI2014] 起床困难综合症 —— 正解:按位拆开,一位一位贪心
//
// ★★★ 全部立论只有一句话:**AND / OR / XOR 都是「逐位」的运算** ——
// 输出的第 b 位只由输入的第 b 位决定,和别的位一点关系都没有。
// ⇒ 于是「选一个 x ≤ m 让结果最大」这个看起来要枚举 10⁹ 种可能的问题,
// 拆成 30 个**互不相干**的小问题:这一位填 0 还是填 1。
//
// ★★ 怎么一次问完 30 位:把 **x = 0** 和 **x = 全 1** 各推过一遍所有门。
// 因为逐位独立,`f(0)` 的第 b 位就是「第 b 位填 0 的结果」,
// `f(全1)` 的第 b 位就是「第 b 位填 1 的结果」。⇒ 两趟 O(n),不是 30 趟。
//
// ★ 然后从高位往低位贪心(高位的 2^b 比它下面所有位加起来还大,所以能拿就拿):
// · 填 0 就能得 1 ⇒ **白拿**,一分钱预算不花;
// · 只有填 1 才得 1 ⇒ 看预算:`used + 2^b ≤ m` 才拿。
// ⚠ 第二条必须是**累计**的 `used + 2^b ≤ m`,不是 `2^b ≤ m`(见 p2114Bit.cpp)。
//
// ⚠ 位宽:t、m ≤ 10⁹ < 2³⁰ ⇒ 从 0 出发、只做 AND/OR/XOR,结果永远 < 2³⁰
// ⇒ 第 0~29 位就够,`int` 完全装得下(这道题不需要 long long)。
#include <bits/stdc++.h>
using namespace std;
static const int B = 30; // 0 ~ 29 位
int main() {
int n; long long m;
if (scanf("%d %lld", &n, &m) != 2) return 0;
unsigned a = 0, b = ~0u; // a = f(0),b = f(全 1)
for (int i = 0; i < n; i++) {
char op[8]; unsigned t;
if (scanf("%7s %u", op, &t) != 2) return 0;
if (op[0] == 'A') { a &= t; b &= t; }
else if (op[0] == 'O') { a |= t; b |= t; }
else { a ^= t; b ^= t; }
}
long long used = 0, ans = 0;
for (int k = B - 1; k >= 0; k--) {
unsigned bit = 1u << k;
if (a & bit) ans += bit; // ★ 填 0 就有 —— 白拿
else if ((b & bit) && used + (long long)bit <= m) { // ⚠ 累计预算
used += bit; ans += bit;
}
}
printf("%lld\n", ans);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

4⚠⚠ 错法一(最深的一个):只扫到 m 的最高位

★★★ 「x ≤ m」限制的是「输入」的位,不是「输出」的位

「初始攻击力最多 m,答案还能超过 m 不成?」—— 这句话听起来天经地义,而题面专门加粗提醒过: 「但在通过防御门之后的攻击力不受 m 的限制」。

一扇 OR 1073741823 就能把结果顶到第 29 位去。而那些位填 0 也能得到 ⇒ 它们根本不花预算,跟 m 有多大一点关系都没有。

★ 触发条件只有一层,而且能写下来:存在一个高于 m 最高位的 b,使 f(0) 的第 b 位是 1。 ⇒ 下一步实测:四档「触发 ≡ 抓获」一格不差。 ★ 而题面那个「测试点 1(m = 0)」恰恰是它最容易露馅的地方 —— m = 0 时它只扫第 0 位。

p2114Low.cpp✗ 错法一:只扫到 m 的最高位

5⚠ 错法二和三:一个预算没累计,一个从低位往高位 —— 而它们的触发条件是同一句话

★★★ 两个完全不同的 bug,被抓的轮次一模一样,而给出的错误答案各不相同
  • 错法二:预算判断写成 2ᵇ ≤ m(没有累计)⇒ 它会超预算多买,答案恒 ≥ 正解;
  • 错法三:贪心从低位往高位走 ⇒ 它会把预算花在便宜又不值钱的位上,答案恒 ≤ 正解。

方向正好相反,可实测下来:

档位 ✗ 错法二被抓 ✗ 错法三被抓 ★ 两者同时被抓的轮数 ★ 被抓时两者输出不同的轮数
0 15 15 15 15
1 14 14 14 14
2 0 0 0 0
3 3 3 3 3

★★★ 32 轮里交集 32、而且 32 轮它们的答案两两不同。 共用的那条触发线是一句话:

存在一个被正解拒绝掉的付费位 k,而它自己单独买得起(2ᵏ ≤ m)。

四档实测 15 / 14 / 0 / 3,和两个错法的抓获数一格不差

⇒ ★★ 这条以前没在书里出现过:两个不同的 bug 可以共用同一条触发线 —— 「同一个触发条件」不等于「同一个 bug」。 ★ 而它有一个实用推论:为错法二造的那一档,替错法三也造好了 (和第 7 章那条「为一个 bug 精心造的档位正是另一个 bug 的盲区」正好相反)。

p2114Bit.cpp✗ 错法二:预算没累计(答案恒 ≥ 正解)
p2114Asc.cpp✗ 错法三:从低位往高位贪(答案恒 ≤ 正解)

6⚠ 错法四:白拿的那一位也去扣预算

★ 触发条件也是一句能写下来的话 —— 四格又一个不差

「填 0 就能得 1」的那些位,输入的那一位是 0 ⇒ 它对 x 的大小没有任何贡献。 把它们也算进 used,等于凭空多花钱,后面真正要花钱的位就买不起了。

★ 触发条件:那些白拿的位,真的把某个正解买得起的付费位挤掉了。 四档实测触发 90 / 124 / 0 / 23 ≡ 抓获 90 / 124 / 0 / 23

p2114Free.cpp✗ 错法四:白拿的也扣预算(答案恒 ≤ 正解)

7★★ 对拍:四档 × 四个错法,十六格「触发 ≡ 抓获」一个不差

p2114Gen.cpp(四档)顺手写的 / t 顶格而 m 很小 / 预算宽裕 / 预算紧
⚠ 四档 × 300 轮(参照物是第 ① 版暴力,它在这四档里 0 次不一致)
档位(每档 300 轮) ✗ 只扫到 m 最高位 ✗ 预算没累计 ✗ 从低位开始 ✗ 白拿也扣预算
0 ★ 顺手写的(m ≤ 100t ≤ 100 105 15 15 90
1 ⚠ t 顶格(< 2³⁰)而 m ≤ 100 285 14 14 124
2 ⚠ 对照档:预算宽裕(AND 63m = 3000 0 0 0 0
3 ⚠ 预算紧(t, m < 2048 92 3 3 23
满足触发条件的轮数 105 / 285 / 0 / 92 15 / 14 / 0 / 3 15 / 14 / 0 / 3 90 / 124 / 0 / 23

★★★ 三条读得出来的结论:

  1. ★★★ 十六格「触发 ≡ 抓获」全中。 四个错法的触发条件都只有一层,而且都能写成一句 关于「f(0) / f(全1) / m」的话。 ⇒ 「≡ 是不是运气,取决于你能不能把它算了什么写成式子」的又一次 —— ★ 而这道题之所以能写成式子,正是因为它逐位独立:每个 bug 都只关乎某一位的取舍。
  2. ★★ 档 2 那一整行 0 是「对照档」,它同时给四个错法做了自检 —— 预算宽裕到「想买的全买得起」时,三个和预算有关的错法结构上不可能出错; 而「只扫到 m 最高位」在那一档也是 0,因为 AND 63 把整个问题压进了低 6 位。
  3. ⚠⚠ 档 2 是被实测打回来重做的:第一版只把 t 压到 < 64m 开到 3000, 以为这就叫「预算宽裕」—— 结果那一档抓了 78 次。 真因是 f(全1) 的高位只有被 AND 清掉才会消失:全是 OR/XOR 的话第 6~29 位仍然是 1, 「想买的位」加起来还是上亿。⇒ 开头强插一扇 AND 63 才真的宽裕。 ⇒ ★★ 第 33 章那条的又一次:生成器注释里写的「这一档没有 X」,也要跑一遍才算数。

8⚠ 官方唯一那组样例,把四个错法全放过了

★ 而每一个「放过」都能说出具体原因

样例是 n = 3m = 10、三扇门 AND 5 / OR 6 / XOR 7,答案 1。

错法 为什么被放过
✗ 只扫到 m 最高位 m = 10 ⇒ 扫到第 3 位;而 f(0) = 1,那个 1 就在第 0 位上 —— 在扫描范围内
✗ 预算没累计 只有一位要拿,根本没有「好几位加起来」这回事
✗ 从低位开始 同上:只有一位,顺序无从谈起
✗ 白拿也扣预算 那一位正是白拿的,后面没有付费位被它挤掉

⇒ ★★ 四条原因归根到底是同一句话:这组样例的答案只有一个二进制位。 一道整页都在讲「按位拆开」的题,样例却只有一位能拆 —— 「这组样例在结构上问不出这个问题」的又一次。

9★ 哪一版就已经能过了

p2114Count.cpp本页所有数字的出处(逐位独立的自检 + 两把尺子)
// P2114 解析页上所有数字的出处。
// ./p2114Count 人话版
// ./p2114Count csv 给 check:viz 用
//
// 三件事:
// ① ★★★ **验「按位独立」这个立论本身** —— 正解、四个错法**全都**建立在
// 「输出的第 b 位只由输入的第 b 位决定」上,所以它们互相对拍验不了这句话
// ([第 37 章 P2085](/sol/p2085/) 那条)。这里拿「真把 x 推一遍」当参照物,
// 和「用 f(0) / f(全1) 逐位拼出来的预测值」比。
// ⚠ 而这个 0 **配了自检**:把其中一扇门换成一个**不是逐位**的运算(加法),当场对不上。
// ② 位宽和类型账:t、m ≤ 10⁹ < 2³⁰ ⇒ 第 0~29 位就够,`int` 装得下。
// ③ 暴力值多少分(题面的测试点表直接给了答案),以及顶格的两把尺子。
#include <bits/stdc++.h>
#include <chrono>
using namespace std;
int main(int argc, char** argv) {
bool csv = (argc > 1 && argv[1][0] == 'c');
mt19937 rng(20260906u);
/* ---------- ① 逐位独立:预测 vs 真跑 ---------- */
int trials = 200000, badPredict = 0, badSelf = 0;
for (int r = 0; r < trials; r++) {
int n = 1 + (int)(rng() % 8u);
vector<int> op(n); vector<unsigned> t(n);
for (int i = 0; i < n; i++) { op[i] = (int)(rng() % 3u); t[i] = rng() % 1073741824u; }
auto apply = [&](unsigned x) {
for (int i = 0; i < n; i++) {
if (op[i] == 0) x &= t[i]; else if (op[i] == 1) x |= t[i]; else x ^= t[i];
}
return x;
};
/* ⚠ 自检用的「假门」:把最后一扇换成加法(**不是**逐位运算)*/
auto applyFake = [&](unsigned x) {
for (int i = 0; i < n; i++) {
if (i + 1 == n) x += t[i];
else if (op[i] == 0) x &= t[i]; else if (op[i] == 1) x |= t[i]; else x ^= t[i];
}
return x;
};
unsigned a = apply(0u), b = apply(~0u);
unsigned fa = applyFake(0u), fb = applyFake(~0u);
unsigned x = rng() % 1073741824u;
unsigned pred = 0, predF = 0;
for (int k = 0; k < 30; k++) {
unsigned bit = 1u << k;
if (((x & bit) ? b : a) & bit) pred |= bit;
if (((x & bit) ? fb : fa) & bit) predF |= bit;
}
if ((pred & 0x3FFFFFFFu) != (apply(x) & 0x3FFFFFFFu)) badPredict++;
if ((predF & 0x3FFFFFFFu) != (applyFake(x) & 0x3FFFFFFFu)) badSelf++;
}
/* ---------- ② 位宽 ---------- */
long long tMax = 1000000000LL, pow30 = 1LL << 30;
int bitsNeeded = 0; while ((1LL << bitsNeeded) <= tMax) bitsNeeded++; // 需要多少位才装得下 10⁹
/* ---------- ③ 顶格:正解走几步 vs 暴力走几步 ---------- */
long long nMax = 100000, mMax = 1000000000LL;
long long okOps = 2 * nMax + 30; // 两趟 O(n) + 30 位贪心
long long bruteOps = nMax * (mMax + 1); // O(nm)
/* 正解顶格实测(自己造一组 n = 10⁵ 的门,跑 1000 遍取平均)*/
vector<int> op(nMax); vector<unsigned> tt(nMax);
for (long long i = 0; i < nMax; i++) { op[i] = (int)(rng() % 3u); tt[i] = rng() % 1073741824u; }
static volatile unsigned sink = 0;
auto t0 = chrono::steady_clock::now();
for (int rep = 0; rep < 1000; rep++) {
unsigned a = 0, b = ~0u;
for (long long i = 0; i < nMax; i++) {
if (op[i] == 0) { a &= tt[i]; b &= tt[i]; }
else if (op[i] == 1) { a |= tt[i]; b |= tt[i]; }
else { a ^= tt[i]; b ^= tt[i]; }
}
sink = a ^ b;
}
(void)sink;
double okMs = chrono::duration<double, milli>(chrono::steady_clock::now() - t0).count() / 1000.0;
if (csv) {
printf("trials,%d\nbadPredict,%d\nbadSelf,%d\n", trials, badPredict, badSelf);
printf("bitsNeeded,%d\n", bitsNeeded);
printf("tFitsIn30,%d\n", tMax < pow30 ? 1 : 0);
printf("okOps,%lld\nbruteOps,%lld\n", okOps, bruteOps);
printf("opsRatio,%lld\n", bruteOps / okOps);
printf("okFast,%d\n", okMs < 20.0 ? 1 : 0);
printf("bruteScore,30\n");
return 0;
}
printf("P2114:全部立论压在「AND / OR / XOR 都是逐位的」这一句上\n\n");
printf(" ① 逐位独立验一遍(%d 组随机门序列 + 随机 x):\n", trials);
printf(" 用 f(0) / f(全1) 逐位拼出来的预测值 ≠ 真跑一遍的结果:%d 组\n", badPredict);
printf(" ⚠ 自检:把最后一扇门换成加法(不是逐位运算)⇒ 对不上 %d 组\n\n", badSelf);
printf(" ② 位宽:t、m ≤ 10⁹ 要 %d 位(< 2³⁰)⇒ 第 0~29 位就够,int 装得下,不用 long long\n\n", bitsNeeded);
printf(" ③ 顶格 n = 10⁵、m = 10⁹:\n");
printf(" ★ 正解 两趟 O(n) + 30 位贪心 = %lld 次基本操作,实测 %.3f 毫秒\n", okOps, okMs);
printf(" ✗ 暴力 O(nm) = %lld 次 ⇒ 差 %lld 倍(而题面测试点表给了它 30 分)\n",
bruteOps, bruteOps / okOps);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★ 第 ② 版(按位贪心)就是终点 —— 而这一页的四个坑全长在同一句话的四个角上
写法 顶格基本操作数 交上去
第 ① 版:枚举所有 x 10¹⁴ 30 分(题面测试点 1、2、3)
★ 正解:两趟 O(n) + 30 位贪心 200030(实测 0.43 毫秒) 100 分
✗ 只扫到 m 最高位 一样 ✗ WA
✗ 预算没累计 一样 ✗ WA(恒 ≥ 正解)
✗ 从低位开始 一样 ✗ WA(恒 ≤ 正解)
✗ 白拿也扣预算 一样 ✗ WA(恒 ≤ 正解)

⇒ ★★★ 这道题是第 46 章那句「按位拆开」最完整的一次现场, 而四个错法恰好把这句话的四个角各碰了一遍:

拆到哪一位为止(不是 m 的位宽,是结果的位宽)/ 哪些位要花钱f(0) 已经给的不花)/ 花的钱要不要累计(要)/ 按什么顺序花(从高位往低位)。

★ 而这四件事都不是位运算的知识 —— 位运算只提供了那一句地基: 这 30 位彼此没有一根线连着,所以可以一根一根单独拨。