目录
a.在单调递增序列a中查找大于等于x的数中最小的一个(即x或x的后继)
b.在单调递增序列a中查找小于等于x的数中最大的一个(即x或x的前驱)
一、概念
二分:二分查找,也称折半搜索,是用来在一个有序数组中查找某一元素的算法。
二、操作说明
1.整数集合上的二分
a.在单调递增序列a中查找大于等于x的数中最小的一个(即x或x的后继)
while(l<r){
int mid=(l+r)>>1; ///等价于除2,且向下取整
if(a[mid]>=x) r=mid;
else l=mid+1;
}
return a[l];
当a[mid]>=x时,r=mid。因为mid可能等于x,也可能大于x,所以要找>=x的最小的数应该从mid到比其更小的数中选,所以将可行区间缩小为左半段。若a[mid]<x,l=mid+1。因为此时a[mid]小于x,所以按要求我们需要从比 a[mid]更大的数中选,故区间缩小至右半段。
b.在单调递增序列a中查找小于等于x的数中最大的一个(即x或x的前驱)
while(l<r){
int mid=(l+r+1)>>1;
if(a[mid]<=x) l=mid;
else r=mid-1;
}
return a[l];
当a[mid]<=x时,l=mid。因为mid可能等于x,也可能小于x,所以要找<=x的最大的数应该从mid到比其更大的数中选,所以将可行区间缩小为右半段。若a[mid]>x,r=mid-1。因为此时a[mid]大于x,所以按要求我们需要从比 a[mid]更小的数中选,故区间缩小至左半段。mid=(l+r+1)>>1,是因为这样能使r-l=1时,不管l=mid还是r=mid-1,循环都能以r==l正常结束。
拓展:mid=(l+r)>>1不会取到r这个值,mid=(l+r+1)>>1不会取到l这个值。利用该性质处理无解的情况,把最初的二分区间[1,n]分别扩大为[1,n+1]和[0,n],把a数组的一个越界的坐标包含进来。如果最后二分终止与扩大后的这个越界下标上,则说明a中不存在所求的数。
2.实数域上的二分
while(r-l>eps){
double mid=(l+r)/2;
if(calc(mid)) r=mid;
else l=mid;
}
精度eps一般根据需要保留的小数位k而定,取eps=10^-(k+2)。
for(int i=0;i<100;i++){
double mid=(l+r)/2;
if(calc(mid)) r=mid;
else l=mid;
}
若精度不容易表示,可采用固定循环次数的二分方法,该方法得到的结果的精度通常比设置的eps更高 。
3.三分求单峰函数极值
单峰函数形如图1,仅有一个极值点,两侧的单调性相反。
![](https://i-blog.csdnimg.cn/blog_migrate/5792b81e92c3eae15affbde345854dd9.png)
a.三分查找极大值点
while(r-l>eps){
double t=(r-l)/3.0;
double lmid=l+t,rmid=r-t;
if(f(lmid)>f(rmid)) r=rmid;
else l=lmid;
}
三分查找极大值点,lmid为l到r之间的三分之一点,rmid为l到r之间的三分之二点。当lmid对应的函数值大于rmid对应的函数值时,说明极大值点出现在rmid的左边,所以让r=rmid。当lmid对应的函数值小于等于rmid对应的函数值时,说明极大值点出现在lmid的右边,所以让l=lmid。
b.三分查找极小值点
while(r-l>eps){
double t=(r-l)/3.0;
double lmid=l+t,rmid=r-t;
if(f(lmid)>f(rmid)) l=lmid;
else r=rmid;
}
三分查找极小值点。当lmid对应的函数值大于rmid对应的函数值时,说明极小值点出现在lmid的右边,所以让l=lmid。当lmid对应的函数值小于等于rmid对应的函数值时,说明极小值点出现在rmid的左边,所以让r=rmid。
三、例题实践
1.整数集合上的二分
a.题目描述
题目来源:acwing
给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。
对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。
如果数组中不存在该元素,则返回 -1 -1
。
输入格式
第一行包含整数 n 和 q,表示数组长度和询问个数。
第二行包含 n 个整数(均在1∼10000 范围内),表示完整数组。
接下来 q 行,每行包含一个整数 k,表示一个询问元素。
输出格式
共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。
如果数组中不存在该元素,则返回 -1 -1
。
数据范围
1≤n≤100000
1≤q≤10000
1≤k≤10000
输入样例:
6 3
1 2 2 3 3 4
3
4
5
输出样例:
3 4
5 5
-1 -1
b.解题思路
思路:通过两次二分来找到k的起始位置和终止位置。
c.代码实现
#include<iostream>
using namespace std;
const int N=1e5+10;
int a[N];
int main(){
int n,q;
cin >>n >>q;
for(int i=0;i<n;i++) cin >>a[i];
while(q--){
int k;
cin >>k;
int l=0,r=n-1;
///找到k的起始位置,等价于在递增序列内找到大于等于k的数中最小的数,区间形如[k,)
while(l<r){
int mid=(l+r)>>1;
if(a[mid]>=k) r=mid;
else l=mid+1;
}
if(a[l]!=k) cout<<"-1 -1" <<endl; ///判断在递增序列内找到的大于等于k的数中最小的数是否为k
else {
cout <<l << ' ';
///找到k的结束位置,等价于在递增序列内找到小于等于k的数中最大的数,区间形如( ,k]
int l1=l,r1=n-1;
while(l1<r1){
int mid=(l1+r1+1)>>1;
if(a[mid]<=k) l1=mid;
else r1=mid-1;
}
cout <<l1 <<endl;
}
}
return 0;
}
2.实数域上的二分
a.题目描述
给定一个浮点数 n,求它的三次方根。
输入格式
共一行,包含一个浮点数 n。
输出格式
共一行,包含一个浮点数,表示问题的解。
注意,结果保留 6 位小数。
数据范围
−10000≤n≤10000
输入样例:
1000.00
输出样例:
10.000000
b.解题思路
思路:使用二分来查找问题的解
c.代码实现
#include<iostream>
using namespace std;
const double eps=1e-8;
int main(){
double x;
cin >>x;
double l=-1000,r=1000;
while(r-l>eps){
double mid=(l+r)/2;
if(mid*mid*mid<=x) l=mid;
else r=mid;
}
printf("%lf",l);
return 0;
}
3.三分求单峰函数极值
a.题目描述
题目描述
题目来源:洛谷
如题,给出一个 N 次函数,保证在范围 [l,r] 内存在一点 x,使得 [l,x] 上单调增,[x,r] 上单调减。试求出 xx 的值。
输入格式
第一行一次包含一个正整数 N 和两个实数 l,r,含义如题目描述所示。
第二行包含 N+1 个实数,从高到低依次表示该 N 次函数各项的系数。
输出格式
输出为一行,包含一个实数,即为 x 的值。若你的答案与标准答案的相对或绝对误差不超过 1e-5 则算正确。
输入输出样例
输入
3 -0.9981 0.5 1 -3 -3 1
输出
-0.41421
说明/提示
对于 100% 的数据,6≤N≤13,函数系数均在[−100,100] 内且至多 15 位小数,∣l∣,∣r∣≤10 且至多 15 位小数。l≤r。
【样例解释】
如图所示,红色段即为该函数 f(x)=x3−3x2−3x+1 在区间 [−0.9981,0.5] 上的图像。
当 x=−0.41421 时图像位于最高点,故此时函数在 [l,x] 上单调增,[x,r] 上单调减,故 x=−0.41421,输出−0.41421。
b.解题思路
思路:利用三分法来找到x
c.代码实现
#include<iostream>
#include<cstdio>
using namespace std;
const double eps=1e-7;
int n;
double l,r,a[20];
double calc(double x)
{
double sum=0;
///计算x带入n次函数的值
for(int i=0;i<=n;i++) sum=sum*x+a[i];
return sum;
}
int main()
{
cin >>n >>l >>r;
for(int i=0; i<=n; i++) cin >>a[i];
while(r-l>eps)
{
double t=(r-l)/3.0;
double lmid=l+t,rmid=r-t;
if(calc(lmid)>calc(rmid)) r=rmid;
else l=lmid;
}
printf("%.5lf\n",l);
return 0;
}