整数域上的二分
代码会随着check函数意义的改变形式而发生改变,这无所谓,但最好遵循一个原则,就是二分中的if判断的最好是我们二分的目标答案的可能值,这样就可以保持形式上的相似,当然改变了也不会错,只是需要对应变化mid的计算方法,容易出错。
模板样式
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
while (l < r)
{
/**
* 之所以在l = mid时,计算mid要+1,是因为当只有两个数据时候r = l + 1,如果mid = l + r >> 1,那么mid = l,区间并没有变化,所以会造成死循环
*/
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
模板应用实例
给定一个按照升序排列的长度为n的整数数组, 返回一个元素k的起始位置和终止位置(位置从0开始计数),如果不存在输出"-1 -1"
以下为3次输入,但3 4 5之前输入的内容是一样的,所以这里只写一遍了
6
1 2 2 3 3 4
3 4 5
对应3 4 5的输出依次为:
3 4
5 5
-1 -1
/**
* 想要理解下面的代码,核心就在于“边界”二字,这道题目需要处理的是比如一个序列比如1 2 2 3 3 4里面有两个3,我们需要
* 分别输出第一个3的位置和第二个3的位置,我们在找第一个3时,它实际上就是一个边界,它左边是<它的数,右边是
* >=它的数,所以我们的判断条件才会是if (a[mid] >= x) 和 else(a[mid] < x),实际上就是根据边界找到它的两个
* 区间,然后如果当前的mid取在了左区间中那么我们就需要将搜索区间向右收缩即l = mid + 1,如果取在了右区间就需要将搜索区间
* 向左收缩,即r = mid,之所以一个+1,一个不加,原因在于边界右区间是>=待寻值得数,包含=的数,所以mid可能也是答案
*/
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n;
int a[N];
int main()
{
cin >> n;
for (int i = 0; i < n; ++ i) cin >> a[i];
int x, l, r, mid;
cin >> x;
l = 0, r = n - 1;
while (l < r)
{
mid = l + r >> 1;
if (a[mid] >= x) r = mid;
else l = mid + 1;
}
if (a[l] != x) cout << "-1 -1" << endl;
else
{
cout << l << " ";
l = 0, r = n - 1;
while (l < r)
{
mid = l + r + 1 >> 1;
if (a[mid] <= x) l = mid;
else r = mid - 1;
}
cout << l << endl;
}
}
浮点数二分
模板样式
bool check(double x) {/* ... */} // 检查x是否满足某种性质
double bsearch_3(double l, double r)
{
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
模板应用实例
- 求平方根
#include <stdio.h>
using namespace std;
int main()
{
double a;
scanf("%lf", &a);
double l = 0, r = a > 1.0 ? a : 1.0;
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求 经验值:当题目要求保留4位小数时取1e-6,5位小数-1e-7,6位小数-1e-8,比位数大2
while (r - l > eps)
{
double mid = (l + r) / 2;
if (mid * mid >= a) r = mid;
else l = mid;
}
printf("%lf
", l);
return 0;
}
- 求立方根
限定输入值n范围为:−10000 ≤ n ≤ 10000
#include <stdio.h>
using namespace std;
int main()
{
double n;
scanf("%lf", &n);
double l = -10000, r = 10000;
const double eps = 1e-8;
while (r - l > eps)
{
double mid = (l + r) / 2;
if (mid * mid * mid >= n) r = mid;
else l = mid;
}
printf("%.6lf
", l);
return 0;
}
个人感悟
- 通过做一些题目,二分给我的感觉就是我们希望找到一个数据,如果是暴力做法就是在它的可能取值范围中一个个判断,但是这个数据有一个特征就是我们可以根据它的情况直接从它的可能取值范围中去掉一部分的可能取值,从而加速寻找过程。
- 二分的关键在于找到答案两侧数据的差异性及单侧数据的相同点,同时满足这两点才可以快速减少一定量的待搜索空间
- 二分实质上是在查找,所以一定要明确要查找的是什么
例如每一个查询的最大美丽值一题中,要找的实际是已有价格中比给定价格小同时距离给定价格最近的值,如果二分查找的对象定为比给定价格小的所有值(包含给定价格和未给定价格),那么实际上并不具备二分的应用条件两段性,但若查找对象定为给定数组,通过二分数组下标,就具备了两段性,可以迅速缩减搜索空间
需要注意的点
- 上述的二分模板在搜索时最终一定是有结果的,但是该结果未必是合法的,搜索结束时一定要想一下搜索结果如果不是合法解决应当如何处理