【算法学习】二分查找与二分答案


二分可以简单分为二分查找与二分答案。
二分查找:在一个有序的数组中进行搜索,时间复杂度为O(nlogn)。
二分答案:在答案的一个范围内进行二分搜索。

二分查找

二分查找模板

首先,我们看一下二分的模板:
模板1:

while(l<r){
	int mid=(l+r)/2;
	if(num[mid]>=target) r=mid;//选取左边区间再次进行搜索
	else l=mid+1;
}

模板2:

while(l<r){
	int mid=(l+r+1)/2;
	if(num[mid]<=target) l=mid;//选取右边区间再次进行搜索
	else r=mid-1;
}

二分的最后退出条件都是l=r
模板1是尽量往左找目标,搜索出来的l是第一个 ≥ \geq x的下标(除了x大于数组最后一个值,此时l和r只能指向数组最后一个值)
模板2是尽量往右找目标,搜索出来的l是最后一个 ≤ \leq x的下标(除了x小于数组第一个值,此时l和r只能指向数组第一个值)
模板3(浮点二分):

while(r-l>1e-5){//需要一个精度保证
	duoble mid=(l+r)/2;
	if(check(mid)) l=mid;//或r=mid
	else r=mid;//或l=mid
}

浮点二分就相对简单多了,因为浮点除法不会取整,所以mid,l,r,都不用加1或减1。

例题1——查找

在这里插入图片描述
java实现:

import java.io.*;

public class Test1 {
	/*
	因为此题判题的数据要求时间,所以需要用一个快的输入输出
	*/
    public static void main(String[] args) throws IOException {
        BufferedReader bf = new BufferedReader(new InputStreamReader(System.in));
        StreamTokenizer st = new StreamTokenizer(bf);
        PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out));
        st.nextToken();
        int n=(int)st.nval;
        st.nextToken();
        int m=(int)st.nval;
        int[] arr=new int[n+1];
        for(int i=1;i<=n;i++){
            st.nextToken();
            arr[i]=(int)st.nval;
        }
        for(int i=0;i<m;i++){
            st.nextToken();
            int num=(int)st.nval;
            int l=1,r=n;
            while(l<r){
                int mid=(l+r)/2;
                if(arr[mid]>=num) r=mid;
                else l=mid+1;
            }
            if(arr[l]==num){
                pw.print(l+" ");
            }else{
                pw.print(-1+" ");
            }
            pw.flush();
        }
    }
}

例2——A-B 数对

在这里插入图片描述

分析:给出了C,我们要找出A和B。我们可以遍历数组,即让每一个值先变成B,然后二分找对应的A首次出现位置,看是否能找到。
如果找到A,那就二分找最后出现的位置,继而,求出A的个数,即数对的个数。
java实现:

import java.util.Arrays;
import java.util.Scanner;
public class Test2 {
    /*
    这道题的解法有两种:
    第一种:两重for循环,去寻找对应的A-B对,O(n*n)
    第二种:一层for循环确定每一个B,然后再二分查找每一个B对应的A,O(nlogn)
     */
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n=in.nextInt();
        int c=in.nextInt();
        int[] num=new int[n];
        for(int i=0;i<n;i++){
            num[i]=in.nextInt();
        }
        //先排序
        Arrays.sort(num);
        int cnt=0;
        for(int i=0;i<n-1;i++){//遍历每一个B,用二分查找去寻找num中的第一个A和最后一个A
            int l=i+1,r=n-1;
            while(l<r){
                int mid=(l+r)/2;
                if(num[mid]>=num[i]+c) r=mid;
                else l=mid+1;
            }
            int st;
            if(num[l]==num[i]+c) st=l;
            else continue;
            //再去寻找最后一个A
            l=st-1;
            r=n-1;
            while(l<r){
                int mid=(l+r+1)/2;
                if(num[mid]<=num[st]) l=mid;
                else r=mid-1;
            }
            cnt+=l-st+1;
        }
        System.out.println(cnt);
    }
}

例3——烦恼的高考志愿

在这里插入图片描述
java实现:

import java.util.Arrays;
import java.util.Scanner;
public class Test3 {
    /*
    烦恼的高考志愿:
    这道题不是去寻找x准确的值,而是寻找离x最近的那个值
    可以用第一个模板,虽然l指向大于等于x的第一个数,但是注意有两种特殊情况:
    1:当l指向最后一个值的时候,x有可能比这个值大,也有可能比这个值小;这时,需要比较倒数第二个值和倒数第一个值哪一个离x近
    2:当l指向第一个值的时候,x小于等于第一个值;这时,就只能取第一个值
     */
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int m=in.nextInt();
        int n=in.nextInt();
        int[] school=new int[m];
        int[] student=new int[n];
        for(int i=0;i<m;i++){
            school[i]=in.nextInt();
        }
        for(int i=0;i<n;i++){
            student[i]=in.nextInt();
        }
        Arrays.sort(school);
        int num=0;
        for(int i=0;i<n;i++){
            int l=0,r=m-1;
            while(l<r){
                int mid=(l+r)/2;
                if(school[mid]>=student[i]) r=mid;
                else l=mid+1;
            }
            if(school[l]==student[i]||l==0){
                num+=Math.abs(school[l]-student[i]);
            }
            else{
                num+=Math.min(Math.abs(school[l]-student[i]),Math.abs(school[l-1]-student[i]));
            }
        }
        System.out.println(num);
    }
}

解题小技巧:
求最小值(最前面的值)–> 尽量往左找 --> 用模板1;
求最大值(最后面的值)–> 尽量往右找 --> 用模板2。

最后,我们再来看一个浮点二分:

例4——银行贷款(浮点二分)

在这里插入图片描述
java实现:

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Scanner;

public class Test4 {
    /*
    浮点查找:在一个范围内寻找答案
     */
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int sum=in.nextInt();
        int t=in.nextInt();
        int mon=in.nextInt();

        double l=0,r=3;
        while(r-l>1e-5){
            double mid=(l+r)/2;
            if(check(mid,sum,t,mon)) r=mid;
            else l=mid;
        }
        BigDecimal rate=BigDecimal.valueOf(l);
        BigDecimal rate1 = rate.multiply(new BigDecimal(100));
        BigDecimal rate2 = rate1.setScale(1, RoundingMode.HALF_UP);
        System.out.println(rate2.toPlainString());
    }
    public static boolean check(double rate,int sum,int t,int m){
        double sumt=sum;
        for(int i=0;i<m;i++){
            sumt=sumt+sumt*rate-t;
        }
        if(sumt>0) return true;
        return false;
    }
}

二分答案

二分查找与二分答案有何区别?
二分查找:在一个已知的有序数据集上进行二分地查找
二分答案:答案有一个区间,在这个区间中二分,直到找到最优答案

什么是二分答案?
答案属于一个区间,当这个区间很大时,暴力超时。但重要的是——这个区间是对题目中的某个量有单调性的,此时,我们就会二分答案。每一次二分会做一次判断,看是否对应的那个量达到了需要的大小。
判断:根据题意写个check函数,如果满足check,就放弃右半区间(或左半区间),如果不满足,就放弃左半区间(或右半区间)。一直往复,直至到最终的答案。
其实,上面二分查找的例4,寻找的那个区间就是答案区间。

适用范围

如何判断一个题是不是用二分答案做的呢?
1、答案在一个区间内(一般情况下,区间会很大,暴力超时)
2、直接搜索不好搜,但是容易判断一个答案可行不可行
3、该区间对题目具有单调性,即:在区间中的值越大或越小,题目中的某个量对应增加或减少。
此外,可能还会有一个典型的特征:求…最大值的最小 、 求…最小值的最大
1、求…最大值的最小,我们二分答案(即二分最大值)的时候,判断条件满足后,尽量让答案往前来(即:让r=mid),对应模板1;
2、同样,求…最小值的最大时,我们二分答案(即二分最小值)的时候,判断条件满足后,尽量让答案往后走(即:让l=mid),对应模板2;

先看一个经典的二分答案入门:

例1——木材加工

在这里插入图片描述
分析:看,答案就在区间(1,100000000)里,就等着我们找呢,暴力肯定超时,那可能就用二分。
满足条件:
1,答案在一个区间里。
2,如果给一个答案,给目标一个小段的长度,很容易判断是否到K个了。
3,具有单调性,目标小段越长,那能切出的段数越少,目标小段越短,能切出的段数越多。而最终需要K个,从而很容易判断一个答案行不行。

一看求啥,求最长长度,最长?这不,关门打狗,模板2! !
那,判断条件?模板2,如果满足判断,l=mid。啥叫满足呢?那肯定是满足需要的段数了呗!
java实现:

import java.util.Scanner;

public class Test11 {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n=in.nextInt();
        int m=in.nextInt();
        int[] num=new int[n];
        int aMax=Integer.MIN_VALUE;
        for(int i=0;i<n;i++){
            num[i]=in.nextInt();
            if(num[i]>aMax)aMax=num[i];
        }
        int l=1,r=aMax;
        while(l<r){
            int mid=(l+r+1)/2;
            if(check(mid,num,m)) l=mid;
            else r=mid-1;
        }
        System.out.println(l);
    }
    public static boolean check(int mid,int[] a,int target){
        int num=0;
        for(int i=0;i<a.length;i++){
            num+=a[i]/mid;
        }
        if(num>=target)return true;
        else return false;
    }
}

例2——跳石头

在这里插入图片描述
求最大?上模板2!! 那,判断条件?
这时候就要注意了,我们二分的是最短距离,通过二分将这个最短距离(答案)最大化。那我们判断的时候肯定要保证mid是最短距离。

如何保证?我们要求抽过石头剩下的石头中,两个石头间的最短距离为mid,那就要保证剩下的任意两个间距都要大于等于mid。要保证这个,那就只能挑间距大于等于mid的石头跳,中间的石头都将会被抽走。

最后,计数可以被抽走的石头。如果可以被抽走的石头个数小于等于需要抽的M个了,就说明满足条件。因为:既然抽了小于M个都能满足剩下的石头中,两石头间的距离都大于等于mid了,那抽M个,更能满足!
java实现:

import java.util.Scanner;
/**
 * 跳石头,用二分最短距离来做,因为要求最短距离的最大值,所以用模板2
 */
public class Test12 {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int dis=in.nextInt();
        int n=in.nextInt();
        int m=in.nextInt();
        int[] a=new int[n+2];
        for(int i=1;i<=n;i++){
            a[i]=in.nextInt();
        }
        a[n+1]=dis;
        if(n==0){
            System.out.println(dis);
        }
        int l=1;
        int r= (int) 1e9;
        while(l<r){
            int mid=(l+r+1)/2;
            if(check(mid,a,m)) l=mid;
            else r=mid-1;
        }
        System.out.println(l);

    }
    public static boolean check(int minDis,int[] a,int m){
        int cnt=0;//需要移走石头的个数
        int now=0;//当前人所在位置
        for(int i=1;i<a.length;i++){
            if(a[i]-a[now]<minDis){
                cnt++;
            }else{
                now=i;
            }
        }
        if(cnt>m)return false;
        return true;
    }
}

例3——丢瓶盖

在这里插入图片描述
分析:距离最近的2个瓶盖距离最大? 最短距离的最大值! 二分!!

看——求最大值,模板二!

判断条件check:与上题不同的是,这题是保证拿走的那些瓶盖之间的最短距离最大(上题是保证剩下的石头最短距离最大,这两个容易混淆。是我没错了… ),那么,遍历的时候,只要满足这次和上次拿的那个瓶盖间距大于等于mid,就可以拿了。这样就保证了我们找的最短距离mid是最短的间距。

最后如果拿出的总瓶盖数大于等于目标值,就说明满足判断。因为:既然拿了超过目标值就能满足拿走的瓶盖间距大于等于mid,那拿目标值(B)个,肯定更能满足!
java实现:

import java.util.Scanner;

public class Test13 {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n=in.nextInt();
        int m=in.nextInt();
        int[] a=new int[n];
        for(int i=0;i<n;i++){
            a[i]=in.nextInt();
        }
        int l=0,r=(int)1e9;
        while(l<r){
            int mid=(l+r+1)/2;
            if(check(mid,a,m)) l=mid;
            else r=mid-1;
        }
        System.out.println(l);
    }
    public static boolean check(int minDis,int[] a,int m){
        int cnt=1;//第一个数肯定是要抽取的
        int now=0;
        for(int i=1;i<a.length;i++){
            if(a[i]-a[now]>=minDis){
                cnt++;
                now=i;
            }
        }
        if(cnt>=m)return true;
        return false;
    }
}

做了上面两题,我们差不多又可以总结出规律了,心里是不是有点小激动?

最大值最小,最小值最大 类问题解题方向:
最短距离最大化问题:保证任意区间距离要比最短距离mid大或相等(这样,mid才是最短距离)即:区间的距离>=mid
最长距离最小化问题:保证任意区间距离要比最大距离mid小或相等(这样,mid才是最大距离)即:区间的距离<=mid
哈哈哈,是不是太有趣啦?

快快,趁热打铁,再来!!

例4——数列分段 Section II

在这里插入图片描述
分析:没错,这次是最大值最小!
求最小值? 哎对,模板1!
判断条件:要保证:每一段的和都小于等于最大值。也就是说,只要这一段的和加上下一个值大于最大值了,那下一个值加不得,得分段!接着段数++;
最后,统计出的总段数(cnt+1)小于等于目标值了,那就算满足;因为,既然分了小于目标值个段都能保证每段的和小于等于最大值,那么分目标值个段肯定还能保证!

还有一个小细节:l,和 r 的初始化。
所有段中的最大和肯定大于等于数列中的最大值(因为最大值最少单成一段,那所有段中的最大的和肯定要大于等于最大值),所以l要初始化为maxa。
同样,所有段中和的最大值,最大不过数列中的所有值的和。
java实现:

import java.util.Scanner;

public class Test14 {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int n=in.nextInt();
        int m=in.nextInt();
        int[] a=new int[n];
        int sum=0;
        int max=Integer.MIN_VALUE;
        for(int i=0;i<n;i++){
            a[i]=in.nextInt();
            sum+=a[i];
            if(a[i]>max)max=a[i];
        }
        int l=max,r=sum;
        while(l<r){
            int mid=(l+r)/2;
            if(check(mid,a,m))r=mid;
            else l=mid+1;
        }
        System.out.println(l);
    }
    public static boolean check(int maxSum,int[] a,int m){
        int cnt=0;
        int sum=a[0];
        for(int i=1;i<a.length;i++){
            if(a[i]+sum>maxSum){
                cnt++;
                sum=a[i];
            }else{
                sum+=a[i];
            }
        }
        if(cnt+1<=m)return true;
        return false;
    }
}

本文全文参考博客:
https://blog.csdn.net/Mr_dimple/article/details/114656142

  • 9
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值