二分+三分专题与边界处理
由于本人经常被二分与三分的各种细节卡死,所以记录一下。 二分如果出错了,注意查看二分的初始范围、二分的判断条件、L/R的赋值、最终二分得到的结果等。有时候二分错误并不是因为二分细节问题,而是题目存在边界,需要特判。
二分:
目前常用的二分有3种写法,区别在于:①闭区间或左闭右开区间 ②退出条件时l==r?或l==r+1
(1)写法一,左闭右开/左开右闭区间,最终l==r
int l = 1, r = n + 1, mid; // 对应区间 [l, r)
// 这种写法最后求出的是最左边的合法的点
while (l < r) {
mid = (l + r) >> 1;
if (OK(mid)) r = mid; // 如果mid是OK的,区间变为[l,mid]
else l = mid + 1; // 否则变为[mid+1,r]
}
// 这种写法求出的是最右边的合法的点
int l = 0, r = n, mid; // 对应区间 (l, r]
while (l < r) {
mid = (l + r + 1) >> 1;
if (OK(mid)) l = mid;
else r = mid - 1;
}
个人比较习惯使用这种方法。
1. 上面给出两种写法,区别就: 已经是合法的了,但是希望找到 更小/更大 的 。所以在写代码的时候,就可以关心于 的值是不是合法的,然后选择让他变大还是变小。
2. 还有一个细节就是初始区间。以左闭右开为例,二分可能会枚举到的范围只有:,所以不用担心 合不合法。如果不断变大,最后 和 都变成右边界。
(2)写法二,闭区间,最终l==r+1,但是碍于边界问题正确的答案应该是 。
// 目标:找到等于target的位置
int l = 1, r = n, mid; // 对应区间 [l, r]
// 这种写法求出最左边的合法的位置
while (l <= r) {
mid = (l + r) >> 1;
if (a[mid] < target) l = mid + 1; // a[mid]<target不合法,所以l=mid+1
else r = mid - 1; // 否则a[mid]>=target,r=mid-1
} // 最后最左边的合法位置是 l
// 这种写法求出最右边的合法的位置
while (l <= r) {
mid = (l + r) >> 1;
if (a[mid] > target) r = mid - 1; // a[mid]>target不合法,所以r=mid-1
else l = mid + 1; // 否则a[mid]<=target,l=mid+1
} // 最后最右边的合法位置是 r
比如说: 。
例一:我们模拟在 数组中找 ,那么肯定返回的是
搜索过程: -> -> -> 。
例二: 如果找 的话,按上面过程,得出的就是 ,如果是 ,那么l=1无疑是对的。
例三:如果我们找的是3呢? 【假设相等时,令r=mid-1】
搜索过程: -> -> 这时的结果仍然是 的
但是我们如果 时,让 会怎么样?
搜索过程: -> -> ->
这个时候结果就是 了,所以使用这种方法时,一定要特别注意等号的位置。
但你时常会见到这样的写法:其中的 取决于如果搜不到合法值,你必须设定的值。
int l = 1, r = n, mid, ans = INIT_ANS;
while (l <= r) {
mid = (l + r) / 2;
if (ok(mid)) ans = mid, l = mid + 1;
else r = mid - 1;
}
cout << ans << endl;
(3)写法三,闭区间,最终 l + 1 == r ,也就是说,区间内剩下两个数就退出了
while(l + 1 < r) {
mid = (l + r) >> 1;
if(OK(mid)) r = mid;
else l = mid;
}
这三种写法各有优略,第一种是 的 使用的,第二种是一种比较稳妥的方法,但是第三种能维护的东西是前两者不能维护的。
比如说:有些题目就是维护间隔的信息,那么应该使用第三种写法。因为 三个数之间有两个间隔,我们要舍去另一半的时候,我们应该舍去的是间隔,而不是mid。若舍去的是 ,应该保留的是 。所以最后区间剩下的是两个数,也就是一个间隔。【这道题就是这么做的:LINK (欧拉序 + 二分) 其它写法也可做】
参考资料:知乎-二分查找有几种方法?
比较喜欢文章里面的一句话:二分无非就是寻找到 四个值中的一个。
实数二分
如果设置精度eps=1e-9,然后最大值达到1e9的话,double/longdouble都是无法精确表示1e18的(这一点可以根据IEEE浮点数了解一下)。
正是因为精度太低的时候无法精确表示,有时候会被卡死循环。【虽然我的写法没被卡过。。。】
while (l + eps < r) {
mid = (l + r) / 2.0;
if (ok(mid)) l = mid;
else r = mid - eps;
} // 我的奇怪写法
所以二分的时候不应该卡精度,而应该卡次数进行二分。
关于这个二分的次数怎么选择:
(1)看看整数部分最大会出现到多大,如果是1e18,整数部分就需要64次左右的二分的
(2)再看看小数点后面需要多少次,如果精度是1e-9,那么至少要 32次。
总的来说,100次完全足够了,如果题目卡时间,可以进一步减少二分次数。
// 精度为 1e-8
auto ok = [&](double mid) { return mid <= 1 + 1e-8; };
double l = 0.0, r = 1e9, mid;
for (int i = 1; i <= 100; i++) {
mid = (l + r) / 2.0;
if (ok(mid)) l = mid;
else r = mid;
}
// 输出 1.00000001 1 + 1e-8
cout << fixed << setprecision(8) << l << endl;
三分:注意三分一定是严格单调的,不能有相等的情况。
这个三分的常数有点高的,如果题目卡常,有可能会G。
常见问题:
(1)三分死循环 - 记得 l=lmid+1,r=rmid-1,因为(r-l)<3的时候,lmid和rmid都等于L、R,如果不加减1,就会G掉。
(2)三分得到的L、R到底哪个才是答案 - 下面的板子得到的L==R
(3)三分得到的L、R到底是峰值的最左侧还是峰值的最右侧 - 这个需要根据if-else的等号进行分析
int l = 1, r = n, lmid, rmid, ans = INF;
while (l < r) { // 这里可以不加等号吗?(有些题目不加等号可以通过,但是具体不清楚)
lmid = l + (r - l) / 3;
rmid = r - (r - l) / 3;
// 这里加等号,三分完之后的L就是峰值的最右侧的位置,如果不加,就是最左侧的位置
if (value(lmid) <= value(rmid)) l = lmid + 1, ans = min(ans, value(lmid));
else r = rmid - 1, ans = min(ans, value(rmid));
}
return l or r; // 如果while循环里面不加等号,最后一定会有l==r
如果令 为循环条件,返回 L 一般都是正确的。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】