当前位置:   article > 正文

二分法(整数二分/实数二分,二分答案/二分查找)

二分法

一、二分法的理论背景

求解非线性方程的一种简单高效的方法:二分法

非线性方程是指 f (x)中含有三角函数、指数函数或其他超越函数(如对数函数,反三角函数,指数函数等)。这些方程很难求得精确解,不过在实际应用中,只要满足一定精度要求的近似解就可以了。此时需要考虑以下两个问题:

(1)根的存在性。定理:函数 f (x) 在闭区间 [a,b] 上连续,且 f (a) · f (b) < 0 ,则 f(x)存在根。

(2)求根。一般有两种方法,搜索法和二分法,这边只介绍二分法。

        二分法:如果确定 f (x) 在闭区间 [a,b] 上连续,且 f (a) · f (b) < 0(也就是根存在的情况下),把 [a,b] 逐次分半,检查每次分半后区间两端点函数值符号的变化,确定有根的区间。

        注意:使用二分法,需要函数满足两个条件:上下界 [a,b] 确定;且函数在 [a,b] 内单调

算法竞赛中有两种二分题型:整数二分和实数二分。整数二分要注意边界问题,避免漏掉答案或者陷入死循环;实数二分要注意精度问题。

二、整数二分

1.基本代码

        通过两个问题来给出整数二分的基本代码:在单调递增序列中找x或x的后继;在单调递增序列中找x或x的前驱

  • 在单调递增序列中找x或x的后继:在单调递增序列a [ ] 中,如果有x,找第一个x所在位置;如果没有x,找第一个比x大的数的位置。如下图所示。

  • 在单调递增序列中找x或x的前驱:在单调递增序列a [ ] 中,如果有x,找最后一个x所在位置;如果没有x,找第一个比x小的数的位置

2.整数二分查找

例题1:数的范围

 

分析:题目中要求我们寻找一个单调递增数列中的x的起始位置和终止位置。我们可以分为两种情况讨论:

寻找x的起始位置,即找第一个x所在位置,也就是在单调递增序列中找x或x的后继;寻找x的终止位置,即找最后一个x所在位置,也就是在单调递增序列中找x或x的前驱

下面给出“在单调递增序列中找x或x的后继”的过程模拟

代码如下: 

  1. //a表示有n个元素的单调递增数组,x为将要查找的目标元素
  2. int find(int a,int n,int x){
  3. int l=0,r=n-1; //定义左右边界
  4. while(l<r){
  5. int mid=l+r>>1;
  6. if(a[mid]>=x)r=mid; //说明目标元素不在mid位置的右边,搜索区间更新为[l,mid]
  7. else l=mid+1; //说明目标元素不在mid位置以及mid位置的左边,搜索区间更新为[mid+1,r]
  8. } //终止于l=r
  9. return l;
  10. }

在单调递增序列中找x或x的后继中,注意:

(1)对mid的计算

处理使用场合可能出现的问题
mid=(l+r)/2l,r>=0;且 l+r 无溢出l+r 可能溢出;负数情况下有向0取整问题
mid=l+(r-l)/2l-r 无溢出若 l,r 一正一负,l-r 可能溢出
mid=l+r>>1l+r 无溢出l+r 可能溢出

(2)对mid的处理

  • 代码中的 l=mid+1 能否写成 l=mid?

        整数二分中存在取整的问题。例如当 l=2,r=3 时,mid=l+r>>1=2,若取 l=mid=2,那么更新后的 l、r 的值仍然不变,while()陷入死循环。所以不能写成 l=mid。

  • mid能否向上取整?

        不能。例如当 l=2,r=3 时,mid=l+r+1>>1=3,若取 r=mid=3,那么更新后的 l、r 的值仍然不变,while()陷入死循环。

结合以上两条,我们可以发现,当 l =mid时,mid不能向 l 取整,而必须向 r 取整;当 r =mid时,mid不能向 r 取整,而必须向 l 取整。否则将有可能出现死循环。

  • 谨慎使用mid=(l+r)/2

若(l+r)是正数,那么mid向下(向 l)取整;若(l+r)是负数,那么mid向上(向 r)取整。所以能在正区间正确运行的代码,在负区间可能出现死循环。

(3)在二分之前需要进行排序(虽然本题不用进行排序)

下面给出“在单调递增序列中找x或x的前驱”的代码:

  1. int find(int a,int n,int x){
  2. int l=0,r=n-1;
  3. while(l<r){
  4. int mid=l+r+1>>1; //注意:由于l=mid,所以mid必须向上取整
  5. if(a[mid]<=x)l=mid;
  6. else r=mid-1;
  7. }
  8. return r;
  9. }

结合在单调递增序列中找x或x的后继,以及在单调递增序列中找x或x的前驱的代码,本题代码如下:

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. const int N=100010;
  4. int a[N];
  5. int main(){
  6. int n,q;
  7. cin>>n>>q;
  8. for(int i=0;i<n;i++)cin>>a[i];
  9. while(q--){
  10. int k;
  11. cin>>k;
  12. //查找x与x的后继
  13. int l=0,r=n-1;
  14. while(l<r){
  15. int mid=l+r>>1;
  16. if(a[mid]>=k)r=mid;
  17. else l=mid+1;
  18. }
  19. if(a[l]!=k){
  20. cout<<"-1 -1"<<endl;
  21. continue;
  22. }else cout<<l<<' ';
  23. //查找x与x的前驱
  24. l=0,r=n-1;
  25. while(l<r){
  26. int mid=l+r+1>>1;
  27. if(a[mid]<=k)l=mid;
  28. else r=mid-1;
  29. }
  30. cout<<r<<endl;
  31. }
  32. return 0;
  33. }

例题2:A-B 数对 - 洛谷

 分析:

C已知,要找到A-B=C

那么问题可以转化为:对于数列中的每一个元素A,是否存在一个数B,使得A-B=C。若存在,求出B的个数。

显然,我们的目标是,从数列中查找一个数,那么可以考虑用二分法

代码如下:

  1. #include<iostream>
  2. #include<algorithm>
  3. using namespace std;
  4. const int N=200010;
  5. int a[N]; //存储正整数数列
  6. int n,c;
  7. long long res; //注意,长度为n的正整数列中,数对最多有n^2/4个,res>2e9,超出int数据范围
  8. int main(){
  9. cin>>n>>c;
  10. for(int i=0;i<n;i++)cin>>a[i];
  11. sort(a,a+n); //先对数列进行排序
  12. //遍历A的值
  13. for(int i=0;i<n;i++){
  14. int b=a[i]-c; //需要查找的目标元素
  15. int st; //储存目标元素的起始位置
  16. int l=0,r=n-1;
  17. //由于c为正整数,所以b一定小于a[i],又因为a数列由小到大排序
  18. //所以目标元素b一定在 [0,i)区间内,所以可以取 l=0,r=i
  19. while(l<r){
  20. int mid=l+r>>1;
  21. if(a[mid]>=b)r=mid;
  22. else l=mid+1;
  23. }
  24. if(a[l]!=b)continue; //如果找不到目标元素,则进入下一个循环
  25. st=l;
  26. l=0,r=n-1;
  27. //由于起始位置st已知,所以目标元素b一定在(st,i)区间内,所以可以取 l=st,r=i
  28. while(l<r){
  29. int mid=l+r+1>>1;
  30. if(a[mid]<=b)l=mid;
  31. else r=mid-1;
  32. }
  33. res+=r-st+1; //注意要+1
  34. }
  35. cout<<res;
  36. return 0;
  37. }

例题3:烦恼的高考志愿 - 洛谷

 代码如下:

  1. #include<iostream>
  2. #include<algorithm>
  3. using namespace std;
  4. const int N=100010;
  5. int a[N],b[N]; //a[]为学校预计分数线;b[]为学生估分
  6. int n,m;
  7. long long res;
  8. int main(){
  9. cin>>m>>n; //m为学校数,n为学生数
  10. for(int i=0;i<m;i++)cin>>a[i];
  11. for(int i=0;i<n;i++)cin>>b[i];
  12. sort(a,a+m); //对学校分数线进行排序
  13. for(int i=0;i<n;i++){ //遍历每个学生的估分
  14. int l=0,r=m-1;
  15. while(l<r){
  16. int mid=l+r>>1;
  17. if(a[mid]>=b[i])r=mid;
  18. else l=mid+1;
  19. }
  20. //当b[i]小于所有学校分数线时,l=r=0,此时l-1越界,所以要特判
  21. if(b[i]<=a[0])res+=a[0]-b[i];
  22. else res+=min(abs(a[l]-b[i]),abs(a[l-1]-b[i]));
  23. }
  24. cout<<res;
  25. return 0;
  26. }

3.整数二分答案

特征1:整数二分法的两个经典模型:最大值最小化(最大值尽量小),最小值最大化(最小值尽量大)

特征2:答案在一个区间内

最大值最小化:

例题1:数列分段 Section II - 洛谷

 

分析:

求最大值的最小化,考虑整数二分答案的方法来解题。

该题涉及到求区间的和,考虑使用前缀和数组(前缀和数组的下标最好从1开始)

1.输入数列的同时,初始化前缀和数组,找到前缀和数组中的最大值sum_max,

2.定义二分答案所在的区间 [l,r],其中 r=sum_max

3.取mid=l+r>>1

4.对于每次二分出的mid,判断mid是否满足条件(check函数判断,若数组每一段的和都<=mid,那么一共需要分成多少段,若分成的段数<=m,说明mid满足条件,反之则不满足)

过程模拟:

 

代码如下:
  1. #include<iostream>
  2. using namespace std;
  3. const int N=100010;
  4. int a[N];
  5. long long sum[N];
  6. int n,m,a_max,sum_max; //a_max存储数列a中的最大值
  7. bool check(int x){
  8. int k=0;
  9. if(x<a_max)return false;
  10. for(int i=1,j=0;i<=n;i++){
  11. if(sum[i]-sum[j]>x){
  12. j=i-1;
  13. k++;
  14. }
  15. }
  16. if(k<m)return true;
  17. else return false;
  18. }
  19. int main(){
  20. cin>>n>>m;
  21. for(int i=1;i<=n;i++){
  22. cin>>a[i];
  23. sum[i]=a[i]+sum[i-1]; //初始化前缀和数组
  24. if(a[i]>a_max)a_max=a[i]; //找到数列中的最大值
  25. if(sum[i]>sum_max)sum_max=sum[i]; //找到数列中的最大值
  26. }
  27. int l=0,r=sum_max;
  28. while(l<r){
  29. int mid=l+r>>1;
  30. if(check(mid))r=mid;
  31. else l=mid+1;
  32. }
  33. cout<<l;
  34. return 0;
  35. }

 其他例题:[TJOI2007] 路标设置 - 洛谷

最小值最大化:

例题1:木材加工 - 洛谷

 分析:

题目要求我们求出 l 的最大值,且 l 在一个区间内,于是可以考虑用二分答案的方法来求解:从区间中找到一个最优答案。

对于样例:n=3 , k=7 , a[ ]={232,124,456}

  1. 首先,二分的区间必须是有序的,所以第一步应该将数组a进行排序。于是 a[0]=124 , a[1]=232,  a[2]=456。且 l 在[0,456]这个区间内。
  2. 定义 l =0 ,r = a [n-1] //即排序后的数组中最大的元素,在区间 [l,r]内不断二分,直到找到最大值,此时最大值为l=r
  3. 对于每次二分,取mid=l + r +1>>1(判断向上取整和向下取整可以参考第四步。由于 l=mid,所以必须向上取整
  4. 判断mid是否满足条件(也就是当木头被切成长度为mid的小段木头时,是否能成功切出k段),若不满足条件(说明小段木头长度为mid的情况下,原木不足以被切成k段),则r=mid-1(因为mid不满足条件,所以不取到mid,于是右边界要-1);若满足条件,则 l=mid(由于mid满足条件,所以能取到mid)

代码如下:

  1. #include<iostream>
  2. #include<algorithm>
  3. using namespace std;
  4. const int N=100010;
  5. int a[N];
  6. int n,k;
  7. bool check(int x){
  8. int res=0; //记录把木头切割为长度均为x的小段木头,可以切出res段
  9. for(int i=0;i<n;i++){
  10. res+=a[i]/x;
  11. }
  12. if(res>=k)return true;
  13. else return false;
  14. }
  15. int main(){
  16. cin>>n>>k;
  17. for(int i=0;i<n;i++)cin>>a[i];
  18. sort(a,a+n);
  19. int l=0,r=a[n-1];
  20. while(l<r){
  21. int mid=l+r+1>>1;
  22. if(check(mid))l=mid;
  23. else r=mid-1;
  24. }
  25. cout<<l<<endl;
  26. return 0;
  27. }
例题2:[NOIP2015 提高组] 跳石头 - 洛谷

 

 代码如下:

  1. #include<iostream>
  2. using namespace std;
  3. const int N=50010;
  4. int a[N]; //岩石的位置
  5. int d,n,m;//d表示起点到终点的距离
  6. bool check(int x){
  7. int j=0,k=0; //j表示目前所在位置,k表示目前已经移走的岩石数量
  8. for(int i=1;i<=n+1;i++){ //易错点!!i<=n+1
  9. //i表示将要跳的岩石位置,要考虑终点。若j位置无法跳到终点,则把j位置的岩石移走
  10. if(a[i]-a[j]<x)k++; //如果由j位置无法跳到i位置,那么把i位置的岩石移走
  11. else j=i; //如果由j位置能够跳到i位置,那么更新当前位置j
  12. }
  13. if(k<=m)return true; //如果移走的岩石总数<=m,说明该跳跃距离满足条件
  14. return false;
  15. }
  16. int main(){
  17. cin>>d>>n>>m;
  18. for(int i=1;i<=n;i++)cin>>a[i];
  19. a[n+1]=d; //a[n+1]表示位于终点的石头
  20. int l=1,r=d; //最短跳跃距离,即最优解所在区间
  21. while(l<r){
  22. int mid=l+r+1>>1;
  23. if(check(mid))l=mid; //因为要求最大值,所以尽可能往右走
  24. else r=mid-1;
  25. }
  26. cout<<l;
  27. return 0;
  28. }

其他例题:进击的奶牛 - 洛谷

三.实数二分

1.实数二分查找

790. 数的三次方根 - AcWing题库

 

代码如下:

  1. #include<iostream>
  2. using namespace std;
  3. int main(){
  4. double n;
  5. cin>>n; //输入一个浮点数
  6. double l=-10000,r=10000; //定义左右边界
  7. //题目要求保留n-2位小数时,写1e-n(记得多2)
  8. //比如说这道题目中,要求保留六位小数,那么写成1e-8
  9. while(r-l>=1e-8){ //也可以直接循环100次
  10. double mid=(l+r)/2;
  11. if(mid*mid*mid>n)r=mid; //若mid的三次方大于n,说明n的三次方根在mid的左侧,所以令r=mid
  12. else l=mid;
  13. }
  14. printf("%.6lf",l);
  15. return 0;
  16. }

四.二分查找的时间复杂度

假设有n个元素,那么二分后每次查找的区间大小为 n , n/2 ,… n/2^k。其中k是循环的次数。

最坏的情况是k次二分之后,每个区间的大小为1时,找到目标元素。令n/2^k=1,得到k=logn

所以二分查找的时间复杂度为O(logn)

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号