说实话,第一次见到"二分答案"这个说法的人,很容易被绕进去——明明二分查找我熟啊,有序数组里找个数,左右指针一夹,O(log n)搞定。但二分答案完全不是一回事。它不是在数组里找某个值,而是在一个答案区间里"猜"最优解,然后写一个判定函数去验证这个解合不合法。这个概念在蓝桥杯里出现频率极高,尤其是省赛和国赛的填空题、编程大题里,几乎每年都有它的影子。很多同学拿到题第一反应是贪心、DP、模拟,一算复杂度直接超时,但如果换成分二分答案的思路,代码量短,思维量小,跑得还飞快。这篇帖子我就把二分答案从思想、适用场景、代码模板,到真题级别的例题拆解和高频坑位,一次讲透。
1. 二分答案的核心思想:从"猜答案"到"验答案"
1.1 二分查找和二分答案到底差在哪
先理清一个基础概念。普通二分查找,操作对象是已经排好序的数组,目标是找到某个元素的下标。它的前提是"数据本身有序",本质是在做检索。
二分答案的操作对象不是数组,而是一个具有单调性的答案空间。你事先不知道答案是多少,但你能确定答案落在一个范围 [L, R] 内。于是你在这个范围里猜一个值 mid,然后写一个函数 check(mid),判断"如果答案是 mid,能不能满足题目的约束条件"。根据 check 的结果,把搜索区间砍半,继续猜,直到逼近最优解。
核心区别一句话:二分查找是在"已知有序数据"里找目标,二分答案是在"未知答案区间"里猜最优值,用判定来代替搜索。这个转化非常关键,因为它把一个"求最值"的问题,硬生生变成了"猜一个值,然后验证它是否可行"的问题。
打个比方。想象你在猜一个数字,规则是对方每次只告诉你"大了"还是"小了"。这个时候你每次报一个数,根据反馈缩小范围,最终锁定目标。二分答案就是这个套路——只不过"大了""小了"的反馈,来自你自己写的 check 函数。
1.2 单调性是二分答案的命根子
为什么二分答案能成立?因为答案的可行性必须随答案值单调变化。什么意思?就是说,如果答案是 X 可行,那么比 X 更"宽松"的答案一定也可行;如果答案是 X 不可行,那比 X 更"严格"的答案一定也不可行。这个性质一旦成立,整个值域就被划成了"一段可行、一段不可行"两段,你才能在中间来回切。
举一个最经典的例子。假设你有 n 根木棒,每根长度不一,现在想把它们切成 m 段长度相等的小段,问每段最长能有多长。注意这个单调性:每段长度如果取得很短,那一定能切出足够多的段数;每段长度取得越长,能切出的段数就越少。于是"能否切出至少 m 段"这个可行性,随答案增大是单调递减的。你就能二分这个长度。
要是没有单调性,比如你这么切就可行,换个不那么严格的答案反而不可行,那就不能二分。这就像猜数字游戏里对手一会儿说"大了"一会儿说"小了",根本不按套路出牌,你没法玩。
1.3 判定函数 check:整个算法的灵魂
二分答案的所有难点,基本上都压在 check 函数上。check(mid) 要做的事情是:给定一个候选答案,判断它是否满足题目约束。这个函数的实现方式没有固定套路,常见的有几种:
- 配合贪心,比如按顺序扫描一遍,统计需要多少步才能满足条件;
- 配合模拟,直接按照规则跑一遍流程,看是否超出限制;
- 配合数据结构,比如用并查集判断连通性,用线段树维护区间特征;
- 配合 DP,在特定情况下做可行性动态规划。
每次二分,都要调用一次 check,整个二分过程大概调用 O(log R) 次。如果答案范围是 1 到 1e9,也就是大概 30 次左右;范围再大点,也就在 60 次以内。所以 check 的复杂度直接决定总复杂度。比较常见的情况是 check 写成 O(n),整体复杂度 O(n log R),对于 n 在 1e5 级别、时间限制 1 秒的题,完全能扛住。
记住一个心法:写出 check 的那一瞬间,你其实已经解决了 80% 的问题。二分框架谁都会写,难点在于你怎么把一个"求最优解"的问题,转换成"给定答案判断是否可行"的问题。
2. 题目特征识别:看到什么信号就该想到二分答案
2.1 两类最典型的问法
在蓝桥杯和各类算法竞赛里,二分答案的题有非常明显的"脸谱"。第一类是最大值最小化,第二类是最小值最大化。凡是题干里出现类似"使得最大值尽可能小"或者"使得最小值尽可能大"的描述,基本可以无脑往二分答案的方向想。
为什么这两类问法天然适配二分答案?因为"最大"和"最小"往往出现在约束关系的两端,而约束关系通常具备单调性。比如你要安排一个序列分成若干段,每一段有一个和,问所有段的和的最大值最小是多少。段和的最大值越大,就越容易把序列分得足够少,可行性单调;段和的最大值越小,分割就越严格,可行性就变差。这个单调性一出现,二分答案就顺理成章。
2.2 答案必须落在一个明确的区间里
能不能二分,还要看答案的搜索空间是否明确。大多数情况下,答案要么是正整数,要么是浮点数,给你一个大概的上下界。比如"每段木棒长度最长"不会超过原始最长那根木棒的长度;"跳跃距离最小值最大"不会超过起点到终点的总距离。上下界明确,二分区间就定下来了。
如果遇到答案范围巨大,比如 0 到 1e18,别慌,照样能二分,只是多循环几次而已。整数二分一次循环砍半,1e18 也就 60 次,依然很快。浮点数答案更是如此,甚至可以直接循环固定次数,比如 100 次,精度远超需求。
2.3 不适合二分答案的情况
也有不适合二分答案的场景,别硬套。
- 答案的可行性和数值之间没有单调关系。比如某些图论问题,改变一个参数后,可行性可能是震荡的,这种二分就是空中楼阁。
- 判定函数比直接求解还难写。如果 check 本身涉及复杂的动态规划或者搜索,那每次二分都要跑一遍重活,整体复杂度可能直接爆炸。
- 数据范围小到直接暴力枚举答案就能过。比如答案范围只有 1 到 1000,直接枚举每一个值然后 check 一下,复杂度也就是 1000 次 O(n),没必要二分。
3. 二分答案代码模板与边界细节
3.1 整数二分的两种写法:一个致命细节决定成败
整数二分最坑的地方就是边界更新。这里直接给你两个模板,背下来,然后理解背后为什么这样写,比临场推导稳得多。
第一种写法:求最小的可行解,也就是在可行域的左端点附近找答案。
bool check(int x) { // 判断答案 x 是否可行 } int main() { int l = 0, r = 1e9; // 根据题目调整 while (l < r) { int mid = (l + r) / 2; if (check(mid)) { r = mid; // 可行则尝试更小的值 } else { l = mid + 1; // 不可行则必须往大走 } } cout << l << endl; }第二种写法:求最大的可行解,也就是在可行域的右端点附近找答案。
bool check(int x) { // 判断答案 x 是否可行 } int main() { int l = 0, r = 1e9; while (l < r) { int mid = (l + r + 1) / 2; // 注意这个 +1 if (check(mid)) { l = mid; // 可行则尝试更大的值 } else { r = mid - 1; // 不可行则往小走 } } cout << l << endl; }为什么第二种写法里 mid 要取 (l + r + 1) / 2?你假设 l = 3,r = 4,如果不加 1,mid = (3+4)/2 = 3。如果 check(3) 可行,执行 l = mid,结果 l 还是 3,r 还是 4,下一轮依旧是 mid = 3,死循环。加了 1 之后,mid = 4,要么 l 变成 4,要么 r 变成 3,区间必然缩小。这是所有整数二分死循环的根源,99% 的二分答案死循环都出自这里。
3.2 浮点数二分的精度控制
浮点数的二分和整数稍微不同,因为你不能指望 l 和 r 最终相等,只能逼近。两种常见控制方式:
第一种:固定循环次数。比如循环 100 次,保证精度。这种方式最稳,因为不会因为答案范围过大而精度不足。
double l = 0, r = 1e9; for (int i = 0; i < 100; i++) { double mid = (l + r) / 2; if (check(mid)) { l = mid; } else { r = mid; } }第二种:精度阈值控制。循环条件写成 while (r - l > eps),其中 eps 取 1e-7 或者更小。这种写法容易踩坑:如果答案范围很大或者很小,r - l 可能永远大于 eps 导致超时,或者过度循环。我个人更推荐固定循环 80 到 100 次,省心,不爆时间。
还有一个重点:输出格式。蓝桥杯这类比赛经常要求保留几位小数,你在输出的时候要小心。保留两位小数输出 3.14 这种,直接用固定精度输出。但如果你算出来的答案是 3.141000,题目要保留 2 位,可能要求四舍五入或者截断,务必看清题目要求。
3.3 check 函数怎么写才高效
check 函数是二分答案里唯一的"变量",它写得好不好直接决定题目能不能过。几个经验:
- 尽量把 check 的时间复杂度压到 O(n) 或者更低。O(n log n) 在里面套一层二分,总复杂度 O(n log n log C),n 到 1e5 级别就开始吃紧了。
- 不要在 check 里做重复初始化。比如每次循环都重新 new 一个数组、清空一个容器,这非常致命。把需要复用的数据结构放到外层,在 check 里只用 O(1) 或 O(n) 的方式更新。
- 能用贪心就用贪心,因为贪心实现简单、常数小。大部分二分答案题目的 check 都是配合某种贪心策略。
3.4 一个通用的题目区间分析技巧
拿到一道题,怎么定 l 和 r?不要随手写 0 和 1e9。先看题面:
- 如果答案是长度、数量,下界通常是 0 或 1,上界是某个显式给定的最大值,比如所有木棒长度之和、两点间总距离、数组最大值;
- 如果答案是浮点数,下界可能是 0,上界要算一下理论极端值;
- 如果答案要求是整数,还要特别注意你最终输出 l 还是 r,取决于你用的是哪个模板。
下面的排查方式可以帮你想清楚:先在心里把答案猜成"中间值",然后手算几组数据,看 check 的结果是不是和题意一致。如果答案从可选范围的最大值开始一直测到最小值,发现可行性是"可行...可行...不可行...不可行"这样的分段,那二分答案完全可以上。
4. 典型例题深度拆解:三道题彻底吃透套路
4.1 例题一:分木棒(最大化可行解)
题目场景:有 n 根长度不一的木棒,长度存在数组 a 里。现在要把它们切成若干段,要求所有段长度相等,段长为正整数,最终至少能得到 k 段。问每段最长能有多长。
这道题是二分答案的入门经典,很多竞赛教程里都有类似题目。先看单调性:段长越大,能切出来的段数越少;段长越小,能切出来的段数越多。"能否得到至少 k 段"这个判定结果随段长增大而单调变化,所以可以二分答案。
check 函数怎么写?非常朴素:遍历每一根木棒,假设当前段长是 mid,那么第 i 根木棒能贡献的段数就是 a[i] / mid(整数除法),累加所有贡献,如果总数大于等于 k,就返回 true,否则 false。
bool check(int mid) { long long cnt = 0; for (int i = 0; i < n; i++) { cnt += a[i] / mid; if (cnt >= k) return true; // 提前退出,省时间 } return false; }注意两个细节:第一,cnt 要开 long long,因为 n 最多 1e5,每根长度 1e9,加起来段数可以轻松超过 int 范围;第二,提前判断 cnt >= k 时立刻返回 true,这是个很小的常数优化,但数据大的时候有效。
二分区间怎么定?l = 1(段长至少为 1),r = 所有木棒长度的最大值(段长不可能超过最长的单根木棒)。这里用的是"求最大可行解"的模板,所以 mid 要加 1。
这道题有个变种是切巧克力,本质一模一样,只是把木棒换成了巧克力块。核心都是"给定一个答案值,统计它在中每个物体里能切出多少份"。这类题做一道,相当于做十道。
4.2 例题二:跳石头(最小值最大化)
这个场景在算法竞赛里流传很广,我见过各种版本的改编。题目大致是:有一条宽度为 L 的河,从起点到终点有若干块石头,位置坐标分别是 p[1] 到 p[n]。现在要移走其中 m 块石头(不能移走起点和终点),使得任意相邻两块石头(包括起点和终点)之间的距离的最小值最大化。问这个最大化的最小值是多少。
先转换为二分思路:假设跳跃距离最小值是 mid,问能不能在移走不超过 m 块石头的前提下,保证任意相邻两块石头之间的距离都不小于 mid。这里"可行性"随 mid 增大单调变化:mid 越小,越容易满足;mid 越大,要移走的石头就越多,超过 m 就不可行了。
check 函数用贪心。思路是从起点开始,维护上一个没被移走的石头位置 last,然后依次向后扫描每一块石头。如果当前石头和 last 之间的距离小于 mid,说明这块石头应该被移走,移走次数 cnt 加一;否则保留这块石头,把 last 更新到当前石头位置。扫描结束后,判断 cnt 是否不超过 m。
bool check(int mid) { int cnt = 0; int last = 0; // 起点位置为 0 for (int i = 0; i < n; i++) { if (p[i] - last < mid) { cnt++; } else { last = p[i]; } } return cnt <= m; }这里还有一个容易忽略的点:终点也要参与判断。扫描完所有石头后,你还需要看"最后一块保留的石头到终点 L 的距离"是否小于 mid。如果小于 mid,理论上最后一段距离不满足条件,但这时你已经没有可以移动的中间石头了,怎么办?正确做法是在石头序列的最后"虚拟地"加入终点,让终点也参与贪心判断。很多初学者在这个细节上丢分,因为样例里刚好没有触发这种情况,但实际数据里一测就炸。
我没有在 check 里显式处理终点,但实际代码里要么把终点的坐标作为一个额外的待判断点,要么在循环结束之后单独再判断一次最后那段距离,两种方式等价。
这道题背后就是"最小值最大化"的典型结构。它和分木棒的区别就在于 check 的贪心策略完全不同,一个是"统计能切出的份数",一个是"统计需要移走的个数"。
4.3 例题三:浮点数答案的二分(切绳子问题)
题目场景:有 n 条绳子,长度都是浮点数,需要切出 k 段长度相等的绳子,问每段最长能有多长,结果保留两位小数。
这题也是经典。整数版本往往简单,但一旦答案变浮点数,很多同学就不知道怎么控制边界和精度。二分思路不变,答案区间是 [0, 最长绳子的长度],check 函数同样是统计每条绳子能贡献的段数。
check 函数的代码:
bool check(double mid) { int cnt = 0; for (int i = 0; i < n; i++) { cnt += (int)(len[i] / mid); if (cnt >= k) return true; } return false; }注意浮点数二分不能简单用 l = mid 或 r = mid 去等,永远不可能等。我的习惯是固定循环 100 次,这样不管答案范围多大,精度都足够。如果题目要求保留两位小数,我在输出时再固定格式,不会因为浮点误差打印出 3.14 而实际期望是 3.14 但二进制表示可能略偏。
这里有一个特别常见的坑:输出精度。竞赛环境里如果用 C++ 的printf("%.2f\n", l),理论上没问题,但二分 100 次后 l 的精度可能在一个极小范围内浮动,某些极端情况下四舍五入的结果会和标准答案差 0.01。稳妥的做法是,算完答案后加上一个极小偏移量再输出,比如l + 1e-9。这是处理浮点数保留位数的老套路了。
4.4 三道题背后的共同套路
这三道题,表面上场景完全不同,一个切木棒,一个跳石头,一个切绳子,但骨架完全一样:
- 确定答案形式(整数/浮点数)和搜索区间;
- 把"最优解"翻译成一个判定条件;
- 写一个 check(mid),用贪心或模拟判断可行性;
- 套二分模板,调整上下界更新方式;
- 小心边界细节,包括 long long、浮点偏移、题目输出格式。
如果你能把这个流程刻在脑子里,遇到任何看起来像二分答案的题,第一步不是急着写代码,而是先定义清楚"什么是可行解"。
5. 蓝桥杯实战中的高频坑与排查实录
5.1 边界条件出错:左边界该取 0 还是 1
很多同学在小数据上盯着样例跑通过就觉得没问题,结果一提交就 WA。最常见的原因之一是下界设错。比如切绳子问题,如果所有绳子都短于 1 米,但题目限制你切出的每段长度至少 1,那下界就应该设 1;再比如分木棒,如果你把 l 设成 0,check(0) 会出现除零问题。
我见过有人把l = 0, r = 1e9一写到底,不去想答案有没有可能为 0。如果题目的答案最小值可以是 0,那没问题;但如果题目要求正整数,你就要把 l 至少设为 1。还有一种情况是上界设小了。比如一个数组里最大值是 1e9,你自信地把上界设为 1e9,结果答案是 1e9 + 1 或者 1e18,那永远二分不到正确答案。
排查技巧:先手动构造一个所有可能答案的极端数据,跑一遍你的二分区间。比如全部数据都是最大值,看你的 r 够不够;全部数据都是最小值,看看 l 是否会导致 check 出错。这种极端测试两三组,边界问题基本都能暴露。
5.2 二分死循环:mid 取整方向惹的祸
死循环问题前面讲过,根源基本都在于"求最大可行解"时 mid 没加 1。但有时候你明明加了 1,还是死循环,那就要看 mid 的计算是否溢出了。
当 l 和 r 都很大,比如 l = 1e9,r = 1e9,l + r可能超出 int 范围。虽然在 C++ 里 int 最大是 21 亿多,两个 10 亿相加到 20 亿还好,但如果是 l = 1e18,那就直接溢出。解决方法很简单,写成mid = l + (r - l) / 2,或者mid = l + (r - l + 1) / 2,避免加法溢出。这不仅是二分答案的坑,是所有二分写法的通用问题。
定位死循环的小技巧:当你的程序卡住不输出,先在脑内模拟一轮 l = 3, r = 4 的情况,看每次 mid 取什么值,l 和 r 会不会更新。如果更新之后 l 还是 3、r 还是 4,基本就是取整方向错了。还有一种情况是 check 函数本身对 mid 完全相同的输入返回了不同结果,那也会造成死循环,但这属于 check 的 bug,需要单独测 check。
5.3 二分超时:不是二分慢,是 check 太重
二分答案很少因为二分本身超时,因为 30 到 60 次循环真的很快。超时基本都出在 check 函数里。
一个典型的例子:check 内部用了排序。比如你在 check 里对数组做一次 O(n log n) 排序,总复杂度变成 O(n log n log C),当 n = 1e5 时大约要做 30 次排序,时间可能飙到接近上限。更好的做法是在二分之前先把数组按某种规则预排序,在 check 里只做线性扫描。
另一个典型的例子:check 内部频繁申请容器。比如每次二分都 new 一个 vector,用完就释放,30 次循环下来内存分配的常数非常夸张,尤其数据量大时直接拖垮性能。把容器放到外头复用,在 check 里更新长度或内容即可。
最后提一个蓝桥杯常见的优化点:输入输出。一些题的数据规模很大,如果用了慢速的 cin / cout 并且没有关闭同步,可能输入就占了大量时间。通常我在竞赛环境下都会加上ios::sync_with_stdio(false); cin.tie(nullptr);,必要时直接用 scanf / printf。二分答案本身不是瓶颈,但输入输出可能是。
5.4 check 的贪心策略写错:样例对了却全盘皆输
这是最隐蔽的坑。二分答案的 check 里用贪心时,贪心策略如果错了,可能小样例侥幸通过,大数据一波带走。我自己的教训是:写完 check 后,不要只跑题目给的样例,要自己构造几个针对性数据,验证贪心选择是不是局部最优。
举例来说,跳石头问题里,如果遇到一块石头距离 last 小于 mid,你是移走它,还是保留它而移走别的石头?常规做法是移走它,因为保留它会限制后续所有石头的最小距离都变大,并不划算。这个"往后看一步"的逻辑是贪心成立的核心。类似的问题还有分木棒时,是否需要优先消耗长木棒、短木棒?大多数情况下顺序无关,因为段长是固定的,每根木棒贡献的数目只跟它自身长度有关。
如果你在 check 里用了某种排序或者优先队列,写完之后花一分钟想三个问题:当前选择会不会影响后续的选择?如果影响,是否有反例?反例能不能构造出来?想不出反例,才敢提交。
6. 从这道题延伸出去:二分答案的各种变形
二分答案远不止切木棒和跳石头。它还能和很多算法组合,形成更新颖的题目。
- 二分 + 前缀和:比如"分割数组使得每个子段和最大值最小"这类题,check 里需要扫描前缀和或者维护当前段的和,是二分答案与贪心的经典结合。
- 二分 + 并查集:比如"给一些边,问在不超过某个权值的情况下能否连通某两个点",可以把边权排序,二分答案,check 里用并查集判断连通性。
- 二分 + BFS/DFS:有些搜索题要求"最小化路径上的最大值",可以先二分答案,然后用 BFS 验证在限制条件下能否到达终点。
- 二分 + DP:当判定本身具有一定动态规划性质时,check 里可能要跑一个线性 DP。整体复杂度可能会变成 O(n log C),在数据范围较小时也能接受。
这些变形看似复杂,但核心永远是那三步:定区间、写 check、套模板。你只要练熟基础的两三种 check 写法,遇到变种题时就多了一分底气和从容。
说一个我自己的体会:二分答案这种题,最大的难度不是代码,而是你敢不敢往这个方向想。很多同学看到"最大值最小"的第一反应是动态规划或者某些高级数据结构,结果绕了远路。我的建议是,做蓝桥杯历年题目的时候,凡是有"最大值最小""最小值最大""最多/最少能拿多少"这类字眼,先把二分答案写一版,再想其他解法。很多时候,二分答案版的代码,是你所有解法里最短最稳的那一个。
最后再分享一个小技巧:如果你在赛场上碰到一道题完全没有思路,但答案形式是"求某个量的最大/最小值",而且你可以判断它出现在一个有限区间里,不妨试试二分答案——哪怕 check 写得粗糙一点,至少能拿到一部分测试点的分数。这个思路在比赛的关键时刻,往往能救回一道题的分。