题单 · 习题解析

洛谷 P1582 倒水

★★★ 答案 = 最小的 `z` 使 `popcount(N+z) ≤ K`,而正解是一行 `n += lowbit(n)`(每一步让最低位那个 1 严格上升 ⇒ **最多 31 步**);★★★ 「输入够、答案够、**只有中间值不够**」——`N ≤ 2×10⁹` 装得进 `int`(余量 7.37%)、答案顶格 1073741823 也装得下,而算的过程要走到 `2³¹`;⚠⚠ 而实测顶格档 **118 轮中间值真的越过了 2³¹,却一轮都没答错** —— 加减法在模 2³² 下同余、答案又装得下 ⇒ 绕出去又绕回来([第 38 章 P3374](/sol/p3374/) 那条),**可那一步是 UB** ⇒ 这是最危险的一类错:它错得没有任何依据,而对拍、样例、`-Wall` 三样都看不出来;★★ 「O(答案) 的暴力肯定超时」这次乘出来是**只超 29%**(1293 毫秒 / 时限 1 秒),而量它的时候自己踩了 [P1965](/sol/p1965/) 那一跤 —— 第一版把 `N`、`K` 写成编译期常量,`-O2` 把循环算穿,量出 254 毫秒(差 5.2 倍);★★ 正解和暴力**共享同一个前提**(最少瓶子数 = popcount)⇒ 它俩对拍验的是零,所以另写了一份 BFS 真把水倒一遍(`n = 1..28` 全枚举,0 个对不上,而这个 0 配了自检);★ 「do…while 至少倒一次」四档「触发 ≡ 抓获」一个不差,而**三组官方样例一个都挡不住它**

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

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

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

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

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

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

题目描述

一天,CC 买了 N 个容量可以认为是无限大的瓶子,开始时每个瓶子里有 1 升水。 接着 CC 发现瓶子实在太多了,于是他决定保留不超过 K 个瓶子。 每次他选择两个当前含水量相同的瓶子,把一个瓶子的水全部倒进另一个里,然后把空瓶丢弃。 (不能丢弃有水的瓶子)

显然在某些情况下 CC 无法达到目标,比如 N = 3K = 1。 此时 CC 会重新买一些新的瓶子(新瓶子容量无限,开始时有 1 升水),以达到目标。

现在 CC 想知道,最少需要买多少新瓶子才能达到目标呢?

输入格式

一行两个正整数 N, K1 ≤ N ≤ 2×10⁹K ≤ 1000)。

输出格式

一个非负整数,表示最少需要买多少新瓶子。

数据范围

1 ≤ N ≤ 2×10⁹K ≤ 1000

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

输入输出样例

输入

3 1

输出

1

N = 3:两瓶合成一瓶 2 升,剩下 2 + 1 两个瓶子,怎么倒都下不到 1 个。 买 1 个新瓶子 ⇒ 总水量 4 ⇒ 4 = 2 + 2 = 4,一个瓶子装得下。

输入

13 2

输出

3

N = 13 = 1101₂ 要 3 个瓶子(8 + 4 + 1)。 买 3 个 ⇒ 16 = 10000₂ ⇒ 1 个瓶子。⚠ 中间的 14 = 1110₂(3 个)、15 = 1111₂(4 个)都不够。

输入

1000000 5

输出

15808

N = 1000000 的二进制里有 7 个 1,要降到 ≤ 5。 ★ 这一组是三组样例里唯一挡住第 ③ 步那个错法的 —— 因为它的 K 不是 1。

1★ 第一步和位运算无关:最少剩几个瓶子 = 总水量的 popcount

★★ 两句话推完,而第二句就是二进制表示的唯一性

每个瓶子里的水量永远是 2 的幂。 开局都是 1 = 2⁰; 每次合并把两个相同的 2ᵃ 并成 2ᵃ⁺¹ —— 归纳一下就完了。

② ⇒ 于是「最后剩几个瓶子」就是「把总水量 n 写成若干个 2 的幂之和,最少要几项」。 而任意两个相同的项都可以再并一次 ⇒ 最优解里各项两两不同 ⇒ 那就是 n 的二进制表示,项数 = popcount(n)

⇒ 买 z 个新瓶子 ⇒ 总水量变成 n + z答案 = 最小的 z ≥ 0,使得 popcount(n + z) ≤ K

★ 到这里为止一个位运算符都没出现 —— popcount结论,不是技巧。

2★ 第一版:一个一个试 z —— 它的答案永远对,而这次「肯定超时」是对的(只对 31%)

p1582Brute.cpp第 ① 版:z 从 0 一个一个往上试
// P1582 ★ 暴力:一个一个试 z —— 答案永远正确,问题只在「跑不跑得完」
//
// ★★ 「O(答案) 的暴力肯定超时」是一句要**乘一遍**才能说的话
// ([第 40 章 P1372](/sol/p1372/)、[第 41 章那三次](/sol/p1217/)都被打回过)。
// 这道题的最坏形状是 **N = 2³⁰+1、K = 1** ⇒ z = 1073741823,约 **10.7 亿次**循环,
// 而循环体只有「加一 + 一条 popcnt 指令 + 一次比较」。
// ⇒ 到底够不够,见 p1582Count.cpp 那一行实测。
#include <cstdio>
using namespace std;
int main() {
long long n, k;
if (scanf("%lld %lld", &n, &k) != 2) return 0;
long long z = 0;
while (__builtin_popcountll(n + z) > k) z++;
printf("%lld\n", z);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠ 「O(答案) 肯定超时」是一句要乘一遍才能说的话 —— 而这次乘出来是 1.29 倍

最坏形状是 N = 2³⁰+1K = 1:下一个 2 的幂是 2³¹,所以 z = 1073741823 —— 10.7 亿次循环,而循环体只有「加一 + 一条 popcnt 指令 + 一次比较」。

本机实测 1293 毫秒,时限 1 秒(A 机 · WSL2 · Linux 6.18-microsoft · 2026-09-06 · 独占)。

⇒ ★ 它确实过不了,但只超 29% —— 这和「差三个数量级」是完全不同的两件事。 第 40 章 P1372第 41 章那三次都栽在同一句想当然上: 复杂度不等于耗时,循环体里有什么才算。

⚠⚠ 而量这个数的时候自己踩了一跤,值得单独记:第一版把 NK 写成了编译期常量, -O2 直接把循环算穿,量出 254 毫秒(真值 1293,差 5.2 倍)。第 42 章 P1965 那条一字不改地又中一次:度量程序里那几个数必须是运行期才知道的值。

3★ 正解:把「一个一个试」换成一行 lowbit

★★★ n += lowbit(n) —— 每一步都让「最低位那个 1」严格往上挪
   n        = 1 0 1 1 0 1 1 1 0 0        popcount = 6
   lowbit   = 0 0 0 0 0 0 0 1 0 0        最低位那个 1(第 46 章第 9 步证过 = n & -n)
   n+lowbit = 1 0 1 1 1 0 0 0 0 0        popcount = 4   <- 最低那一段连续的 1 进位并成了一个

★ 两条性质,一起看才完整:

  1. popcount 不会变大。 加上 lowbit 会把最低那一段连续的 1 全部进位、并成上面的一个 1 —— 一段长度 L 的 1 换成 1 个 1,popcount 减少 L − 1 ≥ 0
  2. ★★ 「最低位那个 1」的位置严格上升。 所以循环最多走 31 步就到 2³¹(popcount = 1)。

⇒ 而「每一步都是当前能走的最小的一步」保证了它给的是最小z: 在到达 n + lowbit(n) 之前的每个数,最低那一段 1 都还原封不动地留在那儿,popcount 只会更大。

★ 实测:抽样 300 万个 NK = 1)最多走 28 步。 ⚠ 而「抽样到的最大」不是「上限」(第 16 章 P1518 那条)—— 真正的上限 31 是上面第 2 条出来的,不是量出来的。

p1582.cpp★ 正解:while + 一行 lowbit
// P1582 倒水 —— 正解:答案就是「最小的 z,使得 popcount(N+z) ≤ K」
//
// ★★ 两步推理,第一步是题目、第二步才是位运算:
// ① **最后剩几个瓶子 = popcount(总水量)。**
// 每次合并把两个相同的水量并成它的两倍 ⇒ 每个瓶子里的水量永远是 2 的幂;
// 总水量 n 固定 ⇒ 最少能剩几个瓶子 = 「把 n 写成若干个 2 的幂之和,最少要几项」
// = n 的二进制里有几个 1 = **popcount(n)**(这就是二进制表示唯一性)。
// ② 买 z 个新瓶子 ⇒ 总水量变成 n + z ⇒ 问的就是「最小的 z 使 popcount(n+z) ≤ K」。
//
// ★★★ 而第 ② 步的高效解法是一行 lowbit:**n += lowbit(n)**。
// 加上最低位那个 1,会让最低的那一段连续的 1 进位并成更高的一个 1
// ⇒ popcount 不会变大,而**最低位那个 1 的位置严格上升** ⇒ 最多走 31 步就到 2 的幂。
// ([第 46 章第 9 步](/ch/46-bitwise/)证过 `lowbit(x) = x & -x`。)
//
// ⚠⚠ 类型这一笔要单独算:N ≤ 2×10⁹ **装得进 int**(余量 7.37%),答案也装得进,
// ⇒ 唯一装不下的是**中间值** —— N 顶格而 K = 1 时要走到 2³¹ = 2147483648。
// ⇒ [「答案装得下、中间值装不下」](/sol/p1044/)的又一个现场,而这次连答案都装得下。
#include <cstdio>
using namespace std;
int main() {
long long n, k;
if (scanf("%lld %lld", &n, &k) != 2) return 0;
long long m = n;
while (__builtin_popcountll(m) > k) m += m & (-m); // ★ 加上 lowbit
printf("%lld\n", m - n);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 两把尺子:步数差 3579 万倍,而秒表只差 1293 毫秒 vs 0 毫秒
顶格 N = 2³⁰+1, K = 1 走几步 本机耗时
第 ① 版(一个一个试) 1073741823 1293 毫秒(✗ 超时限 29%)
★ 正解(lowbit) 30 量不出来

步数差 35 791 394 倍。 而这道题的时限只需要它快 1.3 倍就够了 —— ★ 这也是为什么第 ① 版值得先写:它离及格线很近,近到你会以为「优化一下常数就行」。

4⚠ 错法一:以为「一定要并成一瓶」—— 也就是把 K 看成了 1

★ 题面写的是「保留不超过 K 个瓶子」,而三组样例里只有第三组问得出这件事

把题意读成「最后只剩一瓶」,答案就变成「补到下一个 2 的幂」。

官方样例 K 正解 这一版
3 1 1 1 1 ⚠ 放过(K 本来就是 1)
13 2 2 3 3 放过 —— ★ 13 → 14(3 个 1)→ 16,中间那一步 popcount 没降到 2
1000000 5 5 15808 48576 一测就死

⇒ ★★ 第二组样例被放过的原因很具体,而且不是「K 太小」K = 2 明明不等于 1, 可 13 往上走的路上 popcount 从 3 直接跳到 1,从来没在 2 停过。 ⇒ 「这组样例在结构上问不出这个问题」的又一次。 ★ 而 K = 1 那一档是它能证的精确的 0(那时两个写法就是同一件事)。

p1582Pow.cpp✗ 错法一:无视 K,一路补到 2 的幂

5⚠ 错法二:至少倒一次 —— 三组官方样例一个都挡不住

★ 把 while 写成 do…while,触发条件就是「答案本该是 0」

popcount(N) ≤ K 的时候一瓶都不用买,答案是 0。写成 do…while 就至少加了一次 lowbit。

★ 触发条件只有一层:popcount(N) ≤ K ⇒ 抓获数应当 ≡ 正解输出 0 的轮数(下一步实测四格全中)。

⚠ 而三组官方样例的 popcount 分别是 2 / 3 / 7,K 分别是 1 / 2 / 5 —— 三组全都是 popcount(N) > K ⇒ 一个都挡不住。 ★ 顺带一个能算的边界:N ≤ 2×10⁹ 时 popcount 最大是 30(精确算的,不是抽样) ⇒ K ≥ 30 时答案恒为 0 —— 而题面允许 K 到 1000, ⇒ 顺手让 K 随机取 1~1000 的话,97% 的轮次答案都是 0,这一档它必错。

p1582Do.cpp✗ 错法二:do…while ⇒ 至少倒一次

6★★★ 错法三:用 int —— 118 轮真的溢出了,而它一轮都没答错

★★★ 「输入够、答案够、中间不够」,而中间那一步溢出之后又绕了回来

先把三笔账分开算:

顶格 int 装得下吗
输入 N 2 000 000 000 (余量 7.37%
答案 z 1 073 741 823 ★ 够
中间值 N + z 2 147 483 648 = 2³¹ 差一个数

「答案装得下、中间值装不下」的又一个现场, ★ 而这次比那次更极端:那道题的中间值是答案的 76 倍,这道题只差最后那一步

★★★ 而实测的结果是:它一轮都没答错。 顶格那一档 300 轮里,118 轮的中间值真的越过了 2³¹, 可六个版本逐字节对拍下来,这一列是 0 / 0 / 0 / 0

机理和第 38 章 P3374 一字不差:这一页从头到尾只有加法和减法, 而它们在模 2³² 下同余。中间值绕出去(变成 INT_MIN),最后那句 m - n 又把它绕了回来 —— 因为答案本身装得下

⚠⚠ 但结论不是「这道题可以用 int」—— 恰恰相反

有符号整数溢出在 C++ 里是 未定义行为。上面那个「一轮没错」是 本机、这个编译器、这个优化等级下的一次巧合,不是保证: 编译器完全可以假设 m += m & (-m) 不会溢出,从而把这个循环编译成别的东西。

⇒ ★★★ 所以这一版真正的教训是最危险的那一种它错得没有任何依据,而对拍、样例、-Wall 三样都看不出来。 ⇒ 正解照写 long long,代价是零。

p1582Int.cpp✗ 错法三:用 int —— 溢出了,答案却对

7★★ 验算:正解和暴力共享同一个前提 ⇒ 它俩对拍验的是零

★★★ 所以要有第三条路:真的把水倒一遍

正解和第 ① 版都建立在同一句话上 ——「最少剩几个瓶子 = popcount(总水量)」。 两份代码都从这句话出发 ⇒ 它们互相对拍,验不了这句话本身第 37 章 P2085 那条:参照物和解法共享一个假设,这个对拍验的是零)。

p1582Sim.cpp 走的是完全无关的一条路:从「n 个 1 升的瓶子」出发, BFS 枚举所有可达状态(每步挑两个水量相同的并成一个),记下能达到的最少瓶子数。 它不用 popcount,不用二进制,一行位运算都没有。

结果:n = 1..28 全枚举,和 popcount(n) 对不上的 0 个。

⚠ 而这个 0 配了自检:同一段 BFS 去验一个已知错的说法(「最少恒为 1 瓶」), 当场对不上 23 个(28 个里只有 5 个 2 的幂)。 ⇒ 第 19 章 P2240 立的那条规矩:报「0 次」之前,先证明这段代码是活的。

p1582Sim.cpp★ 第三条路:BFS 真把水倒一遍(不用任何位运算)
// P1582 ★ 验算走一条和位运算完全无关的路:**真的把水倒一遍**
// ./p1582Sim [上限 n] 默认 28
// ./p1582Sim [上限 n] csv
//
// ★★★ 为什么需要它:正解和暴力(p1582Brute)**共享同一个前提** ——
// 「最少剩几个瓶子 = popcount(总水量)」。两份代码都建立在这句话上,
// 所以它们互相对拍**验的是零**([第 37 章 P2085](/sol/p2085/) 那条:
// 参照物和解法共享一个假设,这个对拍什么都没验)。
//
// ⇒ 这一份不用 popcount,也不用二进制:它从「n 个 1 升的瓶子」出发,
// **BFS 枚举所有可达的状态**(每一步:挑两个水量相同的瓶子并成一个),
// 记录能达到的最少瓶子数,再和 popcount(n) 比。
// —— 两条路一行代码都不共享。
#include <bits/stdc++.h>
using namespace std;
int main(int argc, char** argv) {
int NMAX = argc > 1 ? atoi(argv[1]) : 28;
bool csv = (argc > 2 && argv[2][0] == 'c');
int bad = 0, checked = 0, badSelf = 0; // badSelf:拿一个**已知错**的说法(「最少恒为 1」)验这段代码是活的
string detail;
for (int n = 1; n <= NMAX; n++) {
vector<int> start(n, 1);
set<vector<int>> seen;
queue<vector<int>> q;
seen.insert(start); q.push(start);
int best = n;
while (!q.empty()) {
vector<int> cur = q.front(); q.pop();
best = min(best, (int)cur.size());
for (size_t i = 0; i + 1 < cur.size(); i++)
if (cur[i] == cur[i + 1]) { // 有序 ⇒ 相等的必然相邻
vector<int> nx;
for (size_t j = 0; j < cur.size(); j++)
if (j != i && j != i + 1) nx.push_back(cur[j]);
nx.push_back(cur[i] * 2);
sort(nx.begin(), nx.end());
if (seen.insert(nx).second) q.push(nx);
}
}
int pc = __builtin_popcount((unsigned)n);
checked++;
if (best != pc) { bad++; if (detail.size() < 60) detail += to_string(n) + " "; }
if (best != 1) badSelf++; // ⚠ 自检:这一行必须**不是** 0
}
if (csv) {
printf("checked,%d\nbad,%d\nbadSelf,%d\n", checked, bad, badSelf);
return 0;
}
printf("P1582 的立论「最少剩几个瓶子 = popcount(总水量)」——真把水倒一遍验一遍\n\n");
printf(" n = 1 .. %d 全枚举(BFS 所有可达状态,不用任何位运算)\n", NMAX);
printf(" 和 popcount(n) 对不上的:%d 个 %s\n", bad, bad ? detail.c_str() : "");
printf(" ⚠ 自检:同一段 BFS 去验一个**已知错**的说法(「最少恒为 1 瓶」)—— 对不上 %d 个\n", badSelf);
printf("\n ⇒ 这条路和正解、和暴力都不共享任何前提 —— 它验的是「题目 → popcount」那一步本身。\n");
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

8★ 对拍表:四档 × 四个版本

p1582Gen.cpp(四档)顺手写的 / K 照题面随 / K 恒为 1 / 顶格
⚠ 四档 × 300 轮 —— 而这张表上最值钱的是最后一列那四个 0
档位(每档 300 轮) ★ 第 ① 版暴力 ✗ 把 K 当成 1 ✗ do…while ✗ 用 int
0 ★ 顺手写的(N ≤ 10⁶K ≤ 10 0 237 47 0
1 ⚠ K 照题面随(1~1000) 0 300 296 0
2 ⚠ 对照档:K 恒为 1 0 0 0 0
3 ★ 顶格(N ∈ [1.5, 2]×10⁹ 0 182 0 0(⚠ 而 118 轮真的溢出了

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

  1. ★★★ 最后一列的四个 0 是这张表的主角:档 3 里有 118 轮中间值越过了 2³¹, 而那一列一次都没被抓到。⇒ 「它溢出了」和「它算错了」是两件事。 ⚠ 而这次它更危险,因为那次是良性的无符号回绕,这次是 UB
  2. ★★ 「do…while」那一列四格「触发 ≡ 抓获」一个不差(47 / 296 / 0 / 0 ≡ popcount(N) ≤ K 的轮数 47 / 296 / 0 / 0)—— 因为它的触发条件只有一层,而且是一句能写下来的话。
  3. ★★ 档 2 那两个 0 性质完全不同:「把 K 当成 1」在 K = 1 时是能证的精确的 0 (两个写法就是同一件事);而「do…while」在那一档是 0,只是因为 N 随机时 popcount(N) = 1 的概率极低(N ≤ 10⁶ 里只有 20 个 2 的幂)。 ⇒ 一个是「结构上不可能」,一个是「概率低」——造两档就能分开
  4. 档 1 那个 296 提醒了一件事:题面允许 K1000,而 popcount(N) 最大只有 30 ⇒ 照题面随 K 的话,97% 的轮次答案都是 0。 ⇒ ★ 这一档抓 bug 很凶,可它同时说明「照题面随」有时测的几乎全是平凡情形「一致有两种」的近亲)—— 所以档 0 那个「K ≤ 10」也不能撤。

9★ 哪一版就已经能过了

p1582Count.cpp本页所有数字的出处
// P1582 解析页上所有数字的出处。
// ./p1582Count 人话版
// ./p1582Count csv 给 check:viz 用
//
// 四件事:
// ① ★★ **lowbit 那个循环最多走几步** —— 用「最低位那个 1 的位置严格上升」证得 ≤ 31,
// 这里把 N = 1..2×10⁹ 抽样 + 把「交替 1」这类最坏形状算一遍,看实测最大是多少。
// ② ★★★ **暴力(一个一个试 z)到底够不够** —— 「O(答案) 肯定超时」是要乘一遍才能说的话
// ([第 40 章 P1372](/sol/p1372/) 打回过一次)。这里用**步数**这把机器无关的尺子量,
// 秒表只当量级(硬规矩第 5 条)。
// ③ 类型账:`int` 在这道题上「输入够、答案够、中间不够」。
// ④ 两个错法的触发线,以及三组官方样例各挡住了谁。
#include <bits/stdc++.h>
#include <chrono>
using namespace std;
static long long solveOk(long long n, long long k, long long* steps = nullptr) {
long long m = n, s = 0;
while (__builtin_popcountll(m) > k) { m += m & (-m); s++; }
if (steps) *steps = s;
return m - n;
}
int main(int argc, char** argv) {
bool csv = (argc > 1 && argv[1][0] == 'c');
const long long NMAX = 2000000000LL;
/* ---------- ① lowbit 循环最多走几步 ---------- */
long long maxSteps = 0, argN = 0;
mt19937_64 rng(20260906ull);
for (int i = 0; i < 3000000; i++) {
long long n = 1 + (long long)(rng() % (unsigned long long)NMAX);
long long s; solveOk(n, 1, &s);
if (s > maxSteps) { maxSteps = s; argN = n; }
}
/* 「交替 1」那种最坏形状:0b0101…01 */
long long alt = 0; for (int b = 0; b <= 30; b += 2) if ((1LL << b) <= NMAX) alt |= 1LL << b;
long long altSteps; solveOk(alt, 1, &altSteps);
/* ---------- ② 暴力够不够:先算步数,再量秒表 ---------- */
/* ⚠⚠ 这两个数必须是**运行期**才知道的值:写成编译期常量的话,-O2 会把整个循环算穿
([第 42 章 P1965](/sol/p1965/) 那一跤:第一版量出 254 毫秒,真值 1310 毫秒,差 5.2 倍)。*/
volatile long long vN = (1LL << 30) + 1, vK = 1;
long long worstN = vN, worstK = vK; // 最坏形状:z 最大
long long okSteps; long long z = solveOk(worstN, worstK, &okSteps);
/* ⚠ 结果必须落进 volatile,否则 -O2 会把整个循环当死代码删掉
([第 45 章 ops.cpp](/ch/45-estimate/) 同款;第一版量出 0 毫秒就是栽在这儿)。*/
static volatile long long sink = 0;
auto t0 = chrono::steady_clock::now();
long long zz = 0; while (__builtin_popcountll(worstN + zz) > worstK) zz++;
sink = zz; (void)sink;
double bruteMs = chrono::duration<double, milli>(chrono::steady_clock::now() - t0).count();
/* ---------- ③ 类型账 ---------- */
double intMargin = 2147483647.0 / (double)NMAX;
long long midMax = 1LL << 31; // 顶格时中间值要走到 2³¹
long long ansMax = z; // 答案的顶格
/* ---------- ④ 触发线 / 官方样例 ---------- */
struct S { long long n, k; } sam[3] = {{3, 1}, {13, 2}, {1000000, 5}};
string powHit, doHit;
for (int i = 0; i < 3; i++) {
long long a = solveOk(sam[i].n, sam[i].k);
long long p = solveOk(sam[i].n, 1); // ✗ 把 K 当成 1
long long m = sam[i].n; long long d = m & (-m); // ✗ do…while
m += d; while (__builtin_popcountll(m) > sam[i].k) m += m & (-m);
powHit += (p == a ? '.' : 'X');
doHit += (m - sam[i].n == a ? '.' : 'X');
}
/* K ≥ 31 时答案恒为 0(N ≤ 2×10⁹ < 2³¹ ⇒ popcount ≤ 31)*/
/* ★ 这个不用抽样,能精确算:把 M 的某一个 1 改成 0、它下面全填 1,取最大的 popcount */
long long maxPop = __builtin_popcountll(NMAX);
for (int b = 0; b <= 31; b++) if (NMAX >> b & 1) {
long long cand = (NMAX & ~((1LL << (b + 1)) - 1)) | ((1LL << b) - 1);
if (cand >= 1) maxPop = max(maxPop, (long long)__builtin_popcountll(cand));
}
if (csv) {
printf("maxSteps,%lld\n", maxSteps);
printf("altSteps,%lld\n", altSteps);
printf("okSteps,%lld\n", okSteps);
printf("bruteZ,%lld\n", z);
printf("stepRatio,%lld\n", z / max(1LL, okSteps));
printf("bruteBand,%d\n", (bruteMs > 300.0 && bruteMs < 8000.0) ? 1 : 0);
printf("intMargin,%.4f\n", intMargin);
printf("midMax,%lld\n", midMax);
printf("ansMax,%lld\n", ansMax);
printf("intFitsN,%d\n", NMAX < 2147483647LL ? 1 : 0);
printf("intFitsAns,%d\n", ansMax < 2147483647LL ? 1 : 0);
printf("intFitsMid,%d\n", midMax < 2147483647LL ? 1 : 0);
printf("powHit,%s\n", powHit.c_str());
printf("doHit,%s\n", doHit.c_str());
printf("maxPop,%lld\n", maxPop);
return 0;
}
printf("P1582:答案 = 最小的 z 使 popcount(N+z) ≤ K\n\n");
printf(" ① lowbit 循环走几步:抽样 300 万个 N(K=1)最多 %lld 步(N = %lld);\n", maxSteps, argN);
printf(" 「交替 1」那种最坏形状 %lld 走 %lld 步。上界 31 是证出来的。\n\n", alt, altSteps);
printf(" ② 暴力(一个一个试 z)最坏形状 N = %lld、K = %lld:\n", worstN, worstK);
printf(" 答案 z = %lld ⇒ 循环 %lld 次,实测 %.0f 毫秒(时限 1 秒)\n", z, z, bruteMs);
printf(" 而正解只走 %lld 步 ⇒ 步数差 %lld 倍\n\n", okSteps, z / max(1LL, okSteps));
printf(" ③ int 这笔账:N 顶格 %lld < 2147483647(余量 %.2f%%)✓;答案顶格 %lld ✓;\n",
NMAX, (intMargin - 1) * 100, ansMax);
printf(" ⚠ 而中间值要走到 %lld ✗ ——「两头都够,中间不够」\n\n", midMax);
printf(" ④ 三组官方样例(. = 放过,X = 一测就死):把 K 当成 1 → %s;do…while → %s\n",
powHit.c_str(), doHit.c_str());
printf(" N ≤ 2×10⁹ 里 popcount 最大是 %lld(精确算的)⇒ K ≥ %lld 时答案恒为 0\n", maxPop, maxPop);
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果
★ 第 ② 版(lowbit)就是终点 —— 而这道题的三个坑各在一个不同的层面上
写法 顶格步数 结果
第 ① 版:一个一个试 z 1073741823 1293 毫秒 / 时限 1 秒(超 29%)
★ 正解:n += lowbit(n) ≤ 31(实测抽样最多 28) AC
✗ 把 K 当成 1 一样 ✗ WA(★ 第三组样例才挡得住)
do…while 一样 ✗ WA(★ 三组样例一个都挡不住
✗ 用 int 一样 UB —— 本机上一轮没错,而它没有任何依据

⇒ ★★★ 三个坑分别长在三个不同的层面上,而没有一个在「位运算怎么写」上:

读题(「不超过 K 个」不是「只剩一瓶」)/ 边界(本来就够了要输出 0)/ 类型(输入和答案都装得下,只有中间值不装)。

★ 而位运算在这道题里干的事只有一句: 把「一个一个往上试」换成「一步跳到下一个 popcount 可能变小的地方」—— lowbit 在这里不是常数优化,它把 10.7 亿步压成了 30 步。