二分的技巧 | 你的二分为什么死循环了
最近在答疑坊做志愿者,很多大一小朋友来问我二分怎么写。据我观察,类似的问题已经困扰过我和我的无数同学们了。为了今后节省体力、保护嗓子,我决定写一篇博客讲一下二分的技巧,这样下次我可以直接把博客转给问问题的人(
朴素的二分相信大家都很熟悉,无非是每次循环取区间中点mid,再判断答案是在mid左边还是mid右边,递归查找,从而在复杂度内找到答案。
但是在实现二分的时候,很多同学发现:自己的二分死循环了 / 自己搞不清楚自己的逻辑了。接下来我们用一道例题说明一下。
例题1:数组分段
已知一个长度为的数组,把它切分成个连续的段,使得每段之和的最大值最小。求这个最小值。
数据范围:。
二分的思路很简单:二分答案,定义一个min_segments(mid)函数,用来求每段和不超过时,最少划分几段。划分的方法是:从左往右遍历整个数组,如果当前段能放得下(加入之后不会让当前段的和超出),则把加到当前段中,否则新开一段,把放进去。然后根据划分的段数,判断答案在mid左边还是右边。
一个bug,改编自我正在debug的代码
小明看完题,写出了这样一份代码:
long long l = 0, r = 1e14, mid;
while (l < r) {
mid = (l + r) / 2;
if (min_segments(mid) >= m)
l = mid;
else
r = mid;
}
cout << l << endl;
运行之后,他惊奇地发现:自己的二分代码死循环了。大家不妨先暂停阅读,思考一下小明的bug出在哪里?
答案揭晓
问题一:死循环
先不考虑别的问题,只考虑二分的最后一步—— 的情况。此时。假如此时发现 ,那么代码会执行到 这一步,然后继续循环——等等,这不就没改变嘛!怪不得死循环了!
问题二:逻辑问题
其实小明还有一个问题,就是在 if (min_segments(mid) >= m)
这一句。不妨思考一下,如果不成立(也就是说如果),意味着什么呢?意味着我们可以把数组分成小于段,每段之和不超过,所以答案大于等于,看上去没有错。那么如果成立呢?它什么也不能说明!如果,那么固然可能是答案,可是答案可不可能比还小?完全有可能,比如,划分出的这段完全可能每段之和都不超过。当然,答案也可能比还要大。所以这个不等式不能用来判断答案是在左边还是右边。
很多同学在写二分时都踩过上面这两个坑。一些人为了避免逻辑错误,会分“大于m”、“等于m”、“小于m”三种情况讨论,但是这样并没有必要,而且在别的二分题目中很可能无法分出三种情况、只能分出两种。接下来我来讲讲二分到底怎么写,才能尽量不出锅。
所以二分到底怎么写?
第一步:判断是否可行
我见过的所有二分问题都可以只分两种情况讨论:
- 可能是答案;
- 不可能是答案。
例如这道题中,如果,则可能是答案;如果(也就是说不可能分段使得每段和不超过),则不可能是答案。
第二步:判断答案在哪一侧。
在这道题里,如果可能是答案,则实际的答案;如果不可能是答案,则实际的答案。(而在其他题中,情况也可能是:如果可能是答案,则实际的答案;如果不可能是答案,则实际的答案。)
于是我们的代码就改成了:
long long l = 0, r = 1e14, mid;
while (l < r) {
mid = (l + r) / 2;
if (min_segments(mid) > m)
l = mid + 1;
else
r = mid;
}
cout << l << endl;
注意l = mid + 1
一句,意味着这种情况中,实际答案不仅在右边,还不可能是,也就是严格大于。这句代码让答案可能出现的区间从变成了。
第三步:考虑的取整问题
最后一步也是关键的一步。虽然在这道题中,mid = (l + r) / 2
是对的,但是有的题中这样却可能导致死循环。例如,假如对另一道题,我们写出了这样的代码:
long long l = 0, r = 1e14, mid;
while (l < r) {
mid = (l + r) / 2;
if (一些条件)
l = mid;
else
r = mid - 1;
}
cout << l << endl;
那么,仍然考虑的情况,此时。那么如果if中的“一些条件”成立,程序会执行l = mid
——得,又来了,没有改变,死循环了。
对于这种情况,我们不应该写mid = (l + r) / 2
,而应该写mid = (l + r + 1) / 2
,这句的效果就是,即向上取整。无论是向下取整还是向上取整,都不会影响二分复杂度的正确性,但是这一个“+1”之差很可能决定你是否死循环。
例如下面这道题,就可以运用这个技巧:
例题2:x的前驱
已知一个长度为的有序数组,每次询问输入一个,输出中最后一个严格小于数的下标(下标从1开始,如果没有比小的数则输出)。
数据范围:。
正确的代码:
int l = 0, r = n;
while (l < r) {
mid = (l + r + 1) / 2;
if (a[mid] < x)
l = mid;
else
r = mid - 1;
}
cout << l << endl;
博客地址:http://rabbithu.cnblogs.com
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
2017-11-26 POJ 3348 Cows | 凸包——童年的回忆(误)
2017-11-26 ZOJ 1081 Within(点是否在多边形内)| 计算几何