【算法学习】二分查找与二分答案
二分可以简单分为二分查找与二分答案。
二分查找:在一个有序的数组中进行搜索,时间复杂度为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