0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P2114,日期见页头。两边不一致时信原站。
题目描述
21 世纪,许多人得了一种奇怪的病:起床困难综合症……(背景略)drd 的防御战线由 n 扇防御门组成。
每扇防御门包括一个运算 op 和一个参数 t,其中运算一定是 OR、XOR、AND 中的一种,
参数则一定为非负整数。如果还未通过防御门时攻击力为 x,则其通过这扇防御门后攻击力将变为 x op t。
最终 drd 受到的伤害为对方初始攻击力 x 依次经过所有 n 扇防御门后转变得到的攻击力。
由于 atm 水平有限,他的初始攻击力只能为 0 到 m 之间的一个整数
(即他的初始攻击力只能在 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 = 4 → 4 OR 6 = 6 → 6 XOR 7 = 1。
题面算过:取 0/2/4/6/8/10 最终都是 1,取 1/3/5/7/9 都是 0 ⇒ 最大伤害 1。
⚠ 记住这组 —— 第 ⑦ 步会看到它把四个错法全放过了。
1★★★ 关键的一句话:AND / OR / XOR 都是「逐位」的运算
这三个运算的真值表是一位一位算的:(x op t) 的第 b 位 = x 的第 b 位 op t 的第 b 位。
⇒ 连着走 n 扇门也是一样 —— 第 b 位从头到尾都待在第 b 位上,谁也不影响谁。
★ 这一句话把这道题从「在 10⁹ 个 x 里挑一个」变成 30 个互不相干的小问题:
「第 b 位填 0 还是填 1」。
★★ 而这 30 个小问题只要两趟就能全部问完:
把 x = 0 和 x = 全 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 亿倍。这次「肯定超时」是真的。
// 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;}点「运行 ▶」看结果
3★ 正解:从高位往低位,一位一位地贪
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 [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;}点「运行 ▶」看结果
4⚠⚠ 错法一(最深的一个):只扫到 m 的最高位
「初始攻击力最多 m,答案还能超过 m 不成?」—— 这句话听起来天经地义,而题面专门加粗提醒过:
「但在通过防御门之后的攻击力不受 m 的限制」。
一扇 OR 1073741823 就能把结果顶到第 29 位去。而那些位填 0 也能得到
⇒ 它们根本不花预算,跟 m 有多大一点关系都没有。
★ 触发条件只有一层,而且能写下来:存在一个高于 m 最高位的 b,使 f(0) 的第 b 位是 1。
⇒ 下一步实测:四档「触发 ≡ 抓获」一格不差。
★ 而题面那个「测试点 1(m = 0)」恰恰是它最容易露馅的地方 —— m = 0 时它只扫第 0 位。
5⚠ 错法二和三:一个预算没累计,一个从低位往高位 —— 而它们的触发条件是同一句话
- 错法二:预算判断写成
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 的盲区」正好相反)。
6⚠ 错法四:白拿的那一位也去扣预算
「填 0 就能得 1」的那些位,输入的那一位是 0 ⇒ 它对 x 的大小没有任何贡献。
把它们也算进 used,等于凭空多花钱,后面真正要花钱的位就买不起了。
★ 触发条件:那些白拿的位,真的把某个正解买得起的付费位挤掉了。 四档实测触发 90 / 124 / 0 / 23 ≡ 抓获 90 / 124 / 0 / 23。
7★★ 对拍:四档 × 四个错法,十六格「触发 ≡ 抓获」一个不差
| 档位(每档 300 轮) | ✗ 只扫到 m 最高位 | ✗ 预算没累计 | ✗ 从低位开始 | ✗ 白拿也扣预算 |
|---|---|---|---|---|
0 ★ 顺手写的(m ≤ 100,t ≤ 100) |
105 | 15 | 15 | 90 |
1 ⚠ t 顶格(< 2³⁰)而 m ≤ 100 |
★ 285 | 14 | 14 | 124 |
2 ⚠ 对照档:预算宽裕(AND 63 + m = 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 |
★★★ 三条读得出来的结论:
- ★★★ 十六格「触发 ≡ 抓获」全中。 四个错法的触发条件都只有一层,而且都能写成一句
关于「
f(0)/f(全1)/m」的话。 ⇒ 「≡ 是不是运气,取决于你能不能把它算了什么写成式子」的又一次 —— ★ 而这道题之所以能写成式子,正是因为它逐位独立:每个 bug 都只关乎某一位的取舍。 - ★★ 档 2 那一整行 0 是「对照档」,它同时给四个错法做了自检 ——
预算宽裕到「想买的全买得起」时,三个和预算有关的错法结构上不可能出错;
而「只扫到
m最高位」在那一档也是 0,因为AND 63把整个问题压进了低 6 位。 - ⚠⚠ 档 2 是被实测打回来重做的:第一版只把
t压到< 64、m开到 3000, 以为这就叫「预算宽裕」—— 结果那一档抓了 78 次。 真因是f(全1)的高位只有被AND清掉才会消失:全是OR/XOR的话第 6~29 位仍然是 1, 「想买的位」加起来还是上亿。⇒ 开头强插一扇AND 63才真的宽裕。 ⇒ ★★ 第 33 章那条的又一次:生成器注释里写的「这一档没有 X」,也要跑一遍才算数。
8⚠ 官方唯一那组样例,把四个错法全放过了
样例是 n = 3、m = 10、三扇门 AND 5 / OR 6 / XOR 7,答案 1。
| 错法 | 为什么被放过 |
|---|---|
✗ 只扫到 m 最高位 |
m = 10 ⇒ 扫到第 3 位;而 f(0) = 1,那个 1 就在第 0 位上 —— 在扫描范围内 |
| ✗ 预算没累计 | 只有一位要拿,根本没有「好几位加起来」这回事 |
| ✗ 从低位开始 | 同上:只有一位,顺序无从谈起 |
| ✗ 白拿也扣预算 | 那一位正是白拿的,后面没有付费位被它挤掉 |
⇒ ★★ 四条原因归根到底是同一句话:这组样例的答案只有一个二进制位。 一道整页都在讲「按位拆开」的题,样例却只有一位能拆 —— 「这组样例在结构上问不出这个问题」的又一次。
9★ 哪一版就已经能过了
// 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 位彼此没有一根线连着,所以可以一根一根单独拨。