看到「最大值最小」和「最小值最大」就条件反射地想二分 —— 这是今天要练成的肌肉记忆
| 0 - 25 min | 想通「二分答案」到底在二什么(第 1 节) |
| 25 - 60 min | 啃两套模板 + 死循环分析(第 2 节)—— 今天的核心 |
| 60 - 90 min | check 函数怎么写 + 上下界怎么定(第 3、4 节) |
| 90 - 160 min | 做 4 道题,每题限时 20 分钟 |
| 160 - 180 min | 对照题解补齐 + 做自测 + 抄速查卡 |
昨天(Day 22)你学的是二分查找:在一个已经排好序的数组里,快速找某个值的位置。
判断依据是「a[mid] 和 x 谁大」。
今天要往前跨一大步:数组不见了,我们改成在「答案可能的取值范围」里二分,
判断依据是「如果答案是 mid,这件事办得到吗?」。这个判断函数就是传说中的 check()。
二分查找是「在数据里二分」,二分答案是「在答案里二分」—— 内核完全一样,都是「每次砍掉一半」。
你心里想了一个 1 到 100 之间的整数,我猜。我每猜一个数,你只回答「大了」或「小了」。
我绝不会从 1 开始一个一个试,我会先猜 50。你说「小了」,我立刻知道答案在 51~100,一口气砍掉一半; 再猜 75……最多 7 次就猜中。
二分答案就是把这个游戏搬到程序里,只不过「大了 / 小了」换成了 check 函数的 true / false。
把「求最优解」这个大难题,变成「判断某个数可行不可行」这个小问题, 然后用二分把答案的取值范围一次次砍半。
猜数字能玩,是因为有单调性:答「小了」就意味着「比它小的全都不用看了」。
二分答案也一样,必须满足:答案 x 越大(或越小),check(x) 越容易(或越难)成立。 只有这样,check 的结果才会是「一整段 true + 一整段 false」这种形状,二分才有意义。
树高分别是 20、15、10、17,要砍到至少 7 米木材。锯片高度 H 设多少最好?
显然 H 定得越高,被锯下来的部分越少。我们列一下:
| 锯片高度 H | 砍到的木材 | 够 7 米吗? |
|---|---|---|
| 0 | 20+15+10+17 = 62 | ✅ 够 |
| 10 | 10+5+0+7 = 22 | ✅ 够 |
| 15 | 5+0+0+2 = 7 | ✅ 刚好够 |
| 16 | 4+0+0+1 = 5 | ❌ 不够 |
| 20 | 0 | ❌ 不够 |
看到了吗?「够」永远排在前面,「不够」永远排在后面,中间只有一个分界点。 这个分界点左边的最后一个「够」,就是我们要的答案(H = 15)。
在砍树这题里:H 越大 → 越难满足(check 结果从 true 变成 false)。
有些题是反过来的:x 越大 → 越容易满足(例如 P1182 的「每段和不超过 x」,x 越大越好分)。
做题第一步永远是:先在草稿纸上想清楚单调方向,再决定二分怎么写。方向搞反,答案必错。
| 二分查找(昨天) | 二分答案(今天) | |
|---|---|---|
| 二分的对象 | 数组的下标范围 [0, n-1] | 答案的取值范围 [下界, 上界] |
| 判断依据 | a[mid] 和 x 比大小 | check(mid) 返回 true / false |
| 要预先有什么 | 数组有序 | 答案单调(check 的形状是 true…true…false…false) |
| 复杂度 | O(log n) | O(n log 值域)(check 通常是 O(n)) |
| 典型问法 | 「这个数在下标几」 | 「最大能到多少 / 最小能到多少」 |
题目里出现下面这些字眼,八成就是二分答案:
· 「最大值最小」/「最小值最大」
· 「最短跳跃距离的最大值」/「每段和的最大值最小」
· 「求最大的高度 H,使得……」/「求最小的长度 l,使得……」
共同特征:答案是一个数,而且这个数「行不行」很好判断,但直接算出来很难。
这一节请慢一点看。二分答案写挂,90% 都是因为模板用混了或者 mid 取整取反了。
| 类型 | 题目的问法 | check(x) 的单调方向 | 我们要找 |
|---|---|---|---|
| A 类 最大化 |
「最短距离的最大值」 「能架多高」 「能切多长」 |
x 越大越难满足 true…true…true…false…false |
最后一个 true |
| B 类 最小化 |
「每段和的最大值最小」 「空旷指数的最小值」 |
x 越大越容易满足 false…false…true…true…true |
第一个 true |
思路:只要 mid 可行,就把它记到 ans 里,然后壮着胆子往更大的方向试。
int l = 下界, r = 上界; int ans = -1; // 记不住答案时的兜底值 while(l <= r){ int mid = l + (r - l) / 2; // 下取整(这种写法取哪种都行) if(check(mid)){ // mid 可行 → 记下来,往「更大」收 ans = mid; l = mid + 1; } else { // mid 不可行 → 只能往「更小」收 r = mid - 1; } } cout << ans << endl; // ans 就是最大的可行值
两个分支分别是 l = mid + 1 和 r = mid - 1,不管走哪条,区间长度都在变小(至少少 1)。
区间一直在缩短,迟早会变成空区间,l <= r 不成立,循环自然结束。
思路:mid 可行就往更小的方向压(把 r 收到 mid),不可行就只能放大。
int l = 下界, r = 上界; // ★ 前提:check(r) 必须为真! while(l < r){ // 注意是 < 不是 <= int mid = l + (r - l) / 2; // ★ 下取整,必须配 r = mid if(check(mid)) r = mid; // 可行 → 试着更小 else l = mid + 1; // 不可行 → 必须更大 } cout << l << endl; // 循环结束时 l == r,就是答案
r 的初值必须是「一定可行」的值。因为循环里 r 只会被赋成 mid,
我们默认它一直是可行的。
如果 check(r) 本身是 false,这个模板会输出一个根本不可行的答案。
例如 P1182 里 r 取所有数之和(整段不切,一定可行);P3853 里 r 取 L(一块路标都不加,间距也不可能超过 L)。
l = mid 必须配上取整还有一种「求最大可行值」的写法是这样收缩区间的:
while(l < r){
int mid = (l + r) / 2;
if(check(mid)) l = mid; // ← 注意这里是 l = mid,不是 l = mid + 1
else r = mid - 1;
}
这么写本身没错,但 mid 必须改成上取整,否则死循环。我们手动模拟一下就明白了。
| 轮次 | l | r | mid = (l+r)/2 | check(mid) | 动作 |
|---|---|---|---|---|---|
| 第 1 轮 | 3 | 4 | 3 | true | l = mid = 3 |
| 第 2 轮 | 3 | 4 | 3 | true | l = mid = 3 |
| 第 3 轮 | 3 | 4 | 3 | true | l = mid = 3 |
| …… | — | — | — | — | 永远出不去 ⛔ |
原因很简单:l = 3, r = 4 时 (3+4)/2 = 3(整数除法把 3.5 砍成 3)。
于是 l = mid 执行完 l 还是 3,区间 [3, 4] 一点没变,下一轮当然一模一样。
while(l < r){
int mid = (l + r + 1) / 2; // ★★ 多加了 1,向上取整
if(check(mid)) l = mid; // l = 4 → l == r,循环结束
else r = mid - 1;
}
这时候 (3+4+1)/2 = 4,l = mid = 4,l == r,循环干净利落地结束。
出现 l = mid → mid 上取整 (l+r+1)/2
出现 r = mid → mid 下取整 (l+r)/2
也就是:谁被赋成 mid,mid 就朝谁的方向取整。
r = mid 时如果上取整(l=3, r=4 → mid=4),r = 4 区间也不动,
同样死循环 —— 危险是双向的。
实在记不住取整规则,就永远用模板 A(l = mid + 1 / r = mid - 1 / 用 ans 记答案)。
它的两个分支都在「跨过 mid」,区间必然缩短,取哪种 mid 都不会死循环。
模板 B 只是代码短一点,没有本质优势。
另外,mid = l + (r - l) / 2 这种写法比 (l + r) / 2 更安全:
当 l、r 都接近 int 上限时,l + r 会溢出,而 l + (r - l) / 2 不会。养成习惯。
二分的框架就那么几行,真正决定成败的是 check。 竞赛里「check 写错」比「二分写错」常见得多。
今天 4 道题的 check 全是 O(n) 的贪心扫描:从头到尾扫一遍数组,用一个变量累计, 该「切一刀」就切、该「搬走」就搬。扫完跟给定的限制比一比,返回 true / false。
| 题目 | check(x) 在做的事 | 复杂度 |
|---|---|---|
| P1873 | 把 h[i] - x 的正数部分加起来,看有没有 >= M | O(n) |
| P2440 | 把每根木头的 L[i] / x 加起来,看有没有 >= k | O(n) |
| P1182 | 贪心切段:累加和超过 x 就切一刀,看总段数有没有 <= M | O(n) |
| P3853 | 每一段间距 d 要新插 (d-1)/x 块路标,看总数有没有 <= K | O(n) |
铁律一:check(x) 只回答「x 行不行」,不负责找最优解。别在里面又写二分、又求最大值。
铁律二:check 的返回值必须随 x 单调。如果发现「有时候 true 有时候 false 毫无规律」, 那不是二分写错了,是这个量根本不能二分(或者你 check 的思路错了)。
以 P1182 为例:数列 4 2 4 5 1,要求分成 3 段,问「每段和的最大值最小是多少」。
我们猜答案 x = 6。手工跑一遍贪心切段:
结果分成 [4 2] [4] [5 1],共 3 段,3 <= 3 ✅ 所以 x = 6 可行。
再试 x = 5:[4] [2] [4] [5] [1] 要 5 段,5 > 3 ❌ 不可行。
所以 6 就是最小的可行值 —— 和洛谷给的样例答案一模一样。
手算一遍,你就知道自己有没有理解 check。代码只是把手算过程翻译成循环而已。 如果样例都手算不出来,写出来的 check 大概率是错的。
二分答案的下界和上界不是随便拍脑袋的,它有固定套路:
r = mid 那种),还要额外确认 check(上界) 一定为真。| 题目 | 二分的量 | 下界 l | 上界 r | 为什么 |
|---|---|---|---|---|
| P1873 | 锯片高度 H | 0 | max(hi) | 再高就一根都砍不到,木材必然是 0 |
| P2440 | 每段长度 len | 1 | max(Li) | 要求长度是正整数;比最长的木头还长就一段都切不出 |
| P1182 | 每段和上限 x | max(Ai) | sum(Ai) | x 至少得装得下最大的单个数;全放一段一定可行 |
| P3853 | 空旷指数 x | 1 | L | 间距是整数,最小 1;公路总长 L 是任何间距的上限 |
上界开小了:真正的答案在上界外面,二分永远找不到它,输出一个偏小的错误答案。
下界开大了:同理,会漏掉更小的可行解。
另外注意 P1182 的 Ai 可以是 0(题面说的是「非负整数」),
所以下界不能想当然地写 1,要写 max(Ai) 才严密。
P1873 的木材总量、P1182 的区间和都可能超过 int(21 亿)。
凡是「一堆数加起来」的地方,一律用 long long。今天 4 道题里有 3 道都用上了。
下面每道题的代码我都用 g++(-std=c++14 -O2 -Wall)实际编译过,并用洛谷官方样例跑过, 「实测结果」里写的就是真实输出。
题意:有一排树,高度各不相同。伐木机的锯片可以升到整数高度 H, 比 H 高的部分会被锯下来。要拿到至少 M 米木材,问锯片最高能升到多少?
1 ≤ N ≤ 10⁶,1 ≤ M ≤ 2×10⁹,树的高度 ≤ 4×10⁵,所有树的高度总和 > M。
两个提醒:
· N 有 100 万,check 必须是 O(n) 且常数要小,二分的层数是 log(4×10⁵) ≈ 19 层,
总共约 2×10⁷ 次操作,可以过。
· 木材总量可能很大,一定要 long long。
典型的 A 类(最大化)问题:答案 H 越大,砍到的木材越少。
check(H):把每棵树超过 H 的部分加起来,看总数是不是 ≥ M。
这个函数从 H = 0 到 H = max(h) 的取值是「true…true…false…false」,
我们要最后一个 true 对应的 H。
所以用模板 A:可行就 ans = mid; l = mid + 1,不可行就 r = mid - 1。
4 7 20 15 10 17
15
5 20 4 42 40 26 46
36
#include <iostream> using namespace std; int n; long long m; // 需要的木材总量(可能到 2×10^9,必须 long long) int h[1000005]; // 每棵树的高度,n 最大 10^6,数组开在全局 // check(H):锯片升到高度 H 时,砍下的木材总量是否 >= m bool check(long long H){ long long sum = 0; for(int i = 0; i < n; i++){ if(h[i] > H) sum += h[i] - H; // 只有比 H 高的部分才被锯下来 if(sum >= m) return true; // 够了就提前返回:既加速又防止累加溢出 } return false; } int main(){ ios::sync_with_stdio(false); // 关掉 cin/cout 与 scanf 的同步,读入更快 cin.tie(0); cin >> n >> m; int maxh = 0; for(int i = 0; i < n; i++){ cin >> h[i]; if(h[i] > maxh) maxh = h[i]; // 顺路求出上界:最高的那棵树 } // 答案(锯片高度)越大,砍到的木材越少 -> 单调,可以二分 long long l = 0, r = maxh, ans = 0; while(l <= r){ // 模板 A:ans 记录 + 区间跨越 mid long long mid = l + (r - l) / 2; if(check(mid)){ // mid 可行,记下来,再试着锯更高 ans = mid; l = mid + 1; } else { // mid 太高,木材不够,只能往低处找 r = mid - 1; } } cout << ans << endl; return 0; }
官方样例 1 4 7 / 20 15 10 17 → 输出 15 与标准答案完全一致
官方样例 2 5 20 / 4 42 40 26 46 → 输出 36 与标准答案完全一致
极限数据 N = 10⁶、树高随机取 1~4×10⁵、M = 2×10⁹ → 输出 359815,
实测运行 402 ms(约 19 层二分 × 10⁶ 次扫描,符合 O(N log 值域) 的预期)。
另外与暴力枚举(从 0 到 max(h) 逐个试 H)随机对拍 300 组,全部一致。
int 直接爆掉。
代码里用 sum >= m 提前 return 也是一种保护。max(h[i]) 最稳,
也顺便减少了二分层数。cin 记得加 ios::sync_with_stdio(false),不然读入会慢得明显。题意:有 n 根原木,要切成 k 段等长的小段(长度必须是正整数,允许有剩余)。 问每段最长能有多长?如果连 1cm 的都切不出来,输出 0。
1 ≤ n ≤ 10⁵,1 ≤ k ≤ 10⁸,1 ≤ Li ≤ 10⁸。
注意 k 可能非常大(1 亿段),而木头最长才 1 亿厘米 —— 但每根木头最多切出 L[i] / len 段,
累加值用 long long 就够了。关键是别把「切出的段数」当 int 去比 k,累加过程可能中转出 int 范围。
也是 A 类:小段长度 len 越长,能切出的段数越少。
check(len):Σ (L[i] / len) ≥ k 吗?这里的除法是整数除法,
天然就是「这根木头能切几段」,不用额外处理余数。
下界取 1(长度必须是正整数),上界取 max(L[i])。
如果 check(1) 都不成立,那 ans 一直是初值 0,正好输出 0 —— 初值选 0 是关键。
3 7 232 124 456
114
验算:232/114 = 2,124/114 = 1,456/114 = 4,合计 7 段 ✅;
如果 len = 115:232/115 = 2,124/115 = 1,456/115 = 3,合计 6 段 < 7 ❌ 所以答案就是 114。
#include <iostream> using namespace std; int n; long long k; // 需要的小段数量,最大 10^8 int L[100005]; // 每根原木的长度 // check(len):所有原木都切成 len 厘米的小段,能不能凑够 k 段 bool check(long long len){ long long cnt = 0; for(int i = 0; i < n; i++){ cnt += L[i] / len; // 整除:这根原木能切出几段 if(cnt >= k) return true; // 够了就提前退出,后面不用再算 } return false; } int main(){ ios::sync_with_stdio(false); cin.tie(0); cin >> n >> k; int maxl = 0; for(int i = 0; i < n; i++){ cin >> L[i]; if(L[i] > maxl) maxl = L[i]; // 上界:最长的那根原木 } // len 越大,能切出的段数越少 -> 单调 long long l = 1, r = maxl, ans = 0; // ★ ans 初值 0:切不出来时正好输出 0 while(l <= r){ long long mid = l + (r - l) / 2; if(check(mid)){ // 能切够,记下来,再试试切更长 ans = mid; l = mid + 1; } else { r = mid - 1; // 切太长了,段数不够 } } cout << ans << endl; return 0; }
官方样例 3 7 / 232 124 456 → 输出 114 与标准答案完全一致
边界 2 5 / 1 1(两根 1cm 的木头要切 5 段,总共只有 2cm)→ 输出 0 正确
(连 1cm 都切不出来,题面要求输出 0)。
与暴力枚举(len 从 1 试到 max(L))随机对拍 300 组,全部一致。
ans 初值写成 -1:那样「切不出来」时会输出 -1,而题目要的是 0。L[i] / 0 会直接除零崩溃(RE)。长度是正整数,下界就该是 1。int 累加段数:n 最大 10⁵、每根最多切 10⁸ 段,累加过程可能超过 21 亿,用 long long。题意:把一个长度为 N 的正整数数列分成 M 段(每段必须连续), 让「各段和中最大的那个」尽可能小,求这个最小值。
题面例子:4 2 4 5 1 分 3 段。
[4 2][4 5][1] → 段和 6、9、1,最大 9;
[4][2 4][5 1] → 段和 4、6、6,最大 6。最大值的最小可能值就是 6。
1 ≤ N ≤ 10⁵,M ≤ N,Ai < 10⁸,答案不超过 10⁹。
两个坑:
· Ai 是非负整数(题面原话),也就是可能为 0,所以下界不能写死成 1,要写 max(Ai);
· 所有数加起来最多 10⁵ × 10⁸ = 10¹³,必须 long long。
这是今天第一道 B 类(最小化最大值)题,也是「二分答案 + 贪心」的经典组合。
我们二分的量是「允许的每段和上限 x」。check(x) 回答:
在每段和都不超过 x 的前提下,最少要分几段?如果这个段数 ≤ M,那 x 就可行。
「最少分几段」用贪心求:从前往后累加,一旦加上当前数就要超 x,就在这里切一刀。 这是最优的 —— 每一段都塞到装不下为止,段数自然最少。
上限放宽了,每一段能塞更多数 → 段数只会变少,不会变多。
所以 check(x) 的形状是「false…false…true…true」:
x 太小分不出(段数超 M),x 够大就行。
我们要找的是第一个 true → 用模板 B:可行就让 r = mid 往小压。
上下界:l = max(Ai)(任何一段的和都不可能小于最大的单个数),
r = sum(Ai)(全部塞进一段,一定可行,满足模板 B 的前提)。
5 3 4 2 4 5 1
6
#include <iostream> using namespace std; int n, m; int a[100005]; // check(x):限制「每段和都不超过 x」,最少要分成几段?段数 <= m 就算可行 bool check(long long x){ int cnt = 1; // 至少有一段 long long cur = 0; // 当前这一段已经累加的和 for(int i = 0; i < n; i++){ if(a[i] > x) return false; // 一个数自己就超过 x,怎么分都不行 if(cur + a[i] > x){ // 加上就爆了 -> 在这里切一刀 cnt++; cur = a[i]; } else { cur += a[i]; // 还能装得下,继续累加 } } return cnt <= m; // 用的段数不超过 m 就说明 x 可行 } int main(){ ios::sync_with_stdio(false); cin.tie(0); cin >> n >> m; long long sum = 0, mx = 0; for(int i = 0; i < n; i++){ cin >> a[i]; sum += a[i]; if(a[i] > mx) mx = a[i]; } // 「最大值最小」:x 越大越容易可行 -> 单调,二分找最小的可行 x long long l = mx, r = sum; // ★ r = sum 保证 check(r) 一定为真 while(l < r){ // 模板 B:while(l < r) long long mid = l + (r - l) / 2; // 下取整,配合 r = mid 使用 if(check(mid)) r = mid; // 可行,试着更小 else l = mid + 1; // 不可行,必须更大 } cout << l << endl; // 结束时 l == r,就是最小的可行值 return 0; }
官方样例 5 3 / 4 2 4 5 1 → 输出 6 与标准答案完全一致
边界 3 3 / 1 2 3(N = M,每个数自成一段)→ 输出 3 正确(此时段和最大值就是最大的元素)。
极限数据 N = 10⁵、M = 50000、Ai 随机取 0~10⁸ → 输出 134021264
(在题面保证的 10⁹ 以内),实测 76 ms。
与暴力枚举(枚举所有 2N-1 种切分方案)随机对拍 300 组,全部一致。
max(Ai)。while(l < r),不是 l <= r。
写成 <= 会多转一轮,可能输出越界的值。r = mid 写成 r = mid - 1:那就变成模板 A 了,
而且最后没有 ans 记录,输出的是错误的值。收缩方式和「答案落在哪」必须配套。cur + a[i] > x 写成 >=:
题目要求「不超过 x」,等于 x 是合法的,所以必须用 >。题意:一条长 L 的公路上原来有 N 个路标(起点、终点一定有)。 最多可以再增设 K 个路标(位置必须是整数)。定义「空旷指数」= 相邻路标间距的最大值。 问空旷指数最小能压到多少。
2 ≤ N ≤ 100000,0 ≤ K ≤ 100000,0 < L ≤ 10⁷。
注意 K 可以为 0(一个人都不加),这时候答案就是原来最大的那个间距。 另外路标位置是整数,这个条件会影响 check 里的取整方式(下面细说)。
典型的 B 类题:最小化「最大间距」。
二分「允许的最大间距 x」。check(x) 回答:要让所有相邻间距都 ≤ x,
最少要新增几块路标?总数 ≤ K 就可行。
某两块的间距是 d。如果新插 c 块,就把 d 切成 c+1 段。
要让每一小段都 ≤ x,需要 d / (c+1) ≤ x,也就是 c ≥ d/x - 1。
因为 c 要是整数,所以最少的 c = ⌈d/x⌉ - 1。
而 ⌈d/x⌉ = (d-1)/x + 1(整数除法),所以:
cnt = (d - 1) / x; // 一行搞定,整数除法自动下取整
| 间距 d | 允许的最大间距 x | (d-1)/x | 要不要真的能摆得下? |
|---|---|---|---|
| 101 | 51 | 1 | 摆 1 块在 51 处 → 间距 51 和 50,都 ≤ 51 ✅ |
| 101 | 50 | 2 | 摆 2 块在 50、100 处 → 间距 50、50、1 ✅ |
| 100 | 50 | 1 | 摆 1 块在 50 处 → 间距 50、50 ✅ |
| 51 | 51 | 0 | 本来就合格,一块都不用加 ✅ |
如果写成 d / x,当 d 正好是 x 的倍数时会多算一块。
例:d = 50, x = 50。正确答案是 0 块(本来就不超),但 50/50 = 1 会算成 1 块。
用 (d-1)/x:49/50 = 0 ✅ 正确。这个 -1 是本题最大的坑。
上下界:l = 1(间距是整数,最小为 1),r = L(总长是任何间距的上限,
而且 x = L 时一块路标都不用加,必然可行 → 满足模板 B 的前提)。
101 2 1 0 101
51
只有起点 0 和终点 101 两块路标,可以加 1 块。
加在 50 处 → 间距 50、51,最大 51;加在 51 处 → 间距 51、50,最大 51。
想让最大间距到 50,就必须加 2 块(超预算),所以答案是 51。
#include <iostream> using namespace std; int L, n, k; int a[100005]; // 原有路标位置(输入保证递增) // check(x):想让所有相邻路标的间距都 <= x,最少要新增几块路标?<= k 就可行 bool check(long long x){ long long need = 0; for(int i = 1; i < n; i++){ long long d = a[i] - a[i - 1]; // 这一段原来的间距 // 间距 d 用 cnt 块新路标切成 cnt+1 段,需要 d/(cnt+1) <= x // 所以 cnt = ceil(d/x) - 1 = (d-1)/x(整数除法自动下取整) need += (d - 1) / x; // ★ 少写这个 -1 就会 WA if(need > k) return false; // 已经超预算,不用再算了 } return need <= k; } int main(){ ios::sync_with_stdio(false); cin.tie(0); cin >> L >> n >> k; for(int i = 0; i < n; i++) cin >> a[i]; // 「空旷指数」= 相邻路标的最大间距,我们要让它最小 // x 越大越容易做到 -> 单调,二分最小的可行 x long long l = 1, r = L; // ★ r = L 时一块都不用加,一定可行 while(l < r){ long long mid = l + (r - l) / 2; if(check(mid)) r = mid; // 做得到,往更小的方向压 else l = mid + 1; // 做不到,只能放宽 } cout << l << endl; return 0; }
官方样例 101 2 1 / 0 101 → 输出 51 与标准答案完全一致
边界 10 3 0 / 0 3 7 10(K = 0,一块都不许加)→ 输出 4 正确
(三个间距是 3、4、3,最大值 4,没得选)。
与暴力对拍(逐个试 x,用「一块一块往前跳」的独立算法统计所需路标数)随机 300 组,全部一致。
(d-1)/x 写成 d/x:间距正好是 x 的倍数时会多算一块路标,导致答案偏大。ceil(d/x)-1 的另一套算法了 —— 看清「整数」二字。| 错误写法 | 后果 | 正确写法 |
|---|---|---|
while(l<r){ mid=(l+r)/2; if(ok) l=mid; ...} |
l 和 r 相邻时会死循环(TLE),例如 l=3, r=4 → mid=3 → l=3 | mid = l+(r-l+1)/2;(有 l=mid 就上取整) |
| 用模板 B 但忘了「r 必须可行」 (如 P1182 的 r 取 max(A)) |
输出一个根本不成立的答案 | r 取「一定可行」的值 (P1182 取 sum,P3853 取 L) |
| 模板 A 和模板 B 的收缩混着用 ( r = mid 配 l = mid) |
区间可能不缩短 → 死循环;或漏掉答案 | 要么「跨越 mid + ans 记录」,要么「贴住 mid + 最后输出 l」 |
累加和 / 段数用 int |
P1873、P1182 会溢出,得到莫名其妙的负数 | 一律 long long |
| 下界 / 上界拍脑袋乱写 (P1182 下界写 1、P2440 下界写 0) |
答案偏大/偏小;P2440 下界 0 会除零 RE | 下界 = max(单个数) 或 1;上界 = max 或 sum |
| check 的单调方向搞反 (可行时往错误的方向收) |
输出边界值(比如总是输出上界或下界) | 写代码前先问自己:x 变大是更容易还是更难? |
P3853 里写 d / x |
d 是 x 的整数倍时多算一块路标 → 答案偏大 | (d - 1) / x |
check 里写 >= 还是 > 搞混 |
P1182 要求「不超过 x」,写成 >= 会少分一段 |
严格读题:「不超过」用 > 判断超限 |
题目:给 n 个物品的价值,要选出 恰好 k 个,使总价值最大。
答:不能。「选恰好 k 个」这件事和「总价值至少为 x」之间没有单调关系 —— 总价值够了,不代表正好用 k 个物品凑出来的。二分答案的前提是答案具有单调性, 这里 check 的形状是乱的,二分毫无意义。这题是排序 / DP 的活。
int l = 0, r = 100;
while(l < r){
int mid = (l + r) / 2;
if(check(mid)) l = mid;
else r = mid - 1;
}
答:会。因为有 l = mid,mid 必须上取整写成 (l + r + 1) / 2。
举个具体的:当区间收缩到 l = 0, r = 1,若 check(0) 为真,
执行 l = mid = 0,区间还是 [0, 1],下一轮一模一样,永远出不去。
答:因为「每段和的上限 x」必须至少能装下单独一个元素。
如果某个 Ai > x,这个元素不管放进哪一段,那段的和都会超过 x,
所以 x 必然不可行。换句话说,x 的理论最小值就是最大的那个数。
把下界设成 max(Ai) 还能顺便在 check 里省掉一个判断(但写上 if(a[i] > x) return false; 更保险)。
答:用公式 (d-1)/x = 99/25 = 3 块。
验证:插在 25、50、75 处 → 四段间距都是 25,都 ≤ 25 ✅。
如果只用 2 块,最多切成 3 段,100/3 = 33.3 > 25 ❌,确实不够。
公式算出来是 3,手工验证也是 3,对上了。
int l = 下界, r = 上界; // 保证 check(r) == true while(l < r){ int mid = l + (r - l) / 2; if(check(mid)) r = mid; // 可行 → 往更小压 else l = mid + 1; // 不可行 → 必须更大 } cout << l << endl; // l == r 就是答案
★ 记忆点:有 r = mid 就下取整,有 l = mid 就上取整。
【判断能不能二分答案】
1. 答案是一个数,而且题问「最大是多少 / 最小是多少」
2. check(x) == 「如果答案是 x,办得到吗」好写(通常 O(n) 贪心)
3. check 的结果随 x 单调:true…true…false…false 或反过来
三条全中 → 二分答案
【模板 A:求最大的可行值】(最大值最大化 / 最小值最大化)
int l = 下界, r = 上界, ans = -1;
while(l <= r){
int mid = l + (r - l) / 2;
if(check(mid)){ ans = mid; l = mid + 1; }
else r = mid - 1;
}
cout << ans;
// 区间每次都跨越 mid,取哪种取整都安全
【模板 B:求最小的可行值】(最大值最小化)
int l = 下界, r = 上界; // ★ check(r) 必须为 true
while(l < r){
int mid = l + (r - l) / 2; // 下取整,配 r = mid
if(check(mid)) r = mid;
else l = mid + 1;
}
cout << l;
【防死循环口诀】
出现 l = mid → mid = (l + r + 1) / 2 (上取整)
出现 r = mid → mid = (l + r) / 2 (下取整)
记不住就永远用模板 A。
【防溢出】
mid 用 l + (r - l) / 2,不要 (l + r) / 2
凡是「一堆数加起来」的量(木材、段和、间距和)一律 long long
【今天的上下界】
P1873 H ∈ [0, max(h)]
P2440 len∈ [1, max(L)]
P1182 x ∈ [max(A), sum(A)]
P3853 x ∈ [1, L]
【今天 check 的三个小公式】
砍树: sum(max(0, h[i] - H)) >= M
切木头: Σ (L[i] / len) >= k
切段: 累加超过 x 就切一刀,段数 <= M
插路标: Σ ((d - 1) / x) <= K // ★ 别忘了 -1
二分与数据结构综合 —— 把二分、前缀和、贪心、DP 串起来用。
明天四道题各有各的味道:
while(l <= r),改成固定循环 100 次最稳。今天先把两套模板写熟,明天就不会被实数二分和前缀和搞晕。