二分查找
二分的思想在程序设计中有着广泛的应用,例如,排序算法中的快速排序、归并排序,数据结构中的二叉树、堆、线段树等。二分是一种常用且高效的算法,它的基本用途是在单调序列中进行查找和判定操作。
对于n个有序且没有重复的元素(假设为升序),从中查找特定的某个元素x,我们可以将有序序列分成规模大致相等的两部分,然后取中间元素与要查找的元素x进行比较,如果x等于中间元素,则查找成功,算法终止;如果x小于中间元素,则在序列的前半部分继续查找,否则在序列的后半部分继续查找。这样就可以将查找的范围缩小一半,然后在剩余的一半中继续重复上面的方法进行查找。
这种每次都从中间元素开始比较,并且一次比较后就能把查找范围缩小一半的方法,叫作二分查找。二分查找的时间复杂度是 O(logN),是一种效率较高的查找算法。
时间复杂度O(logN)
在常见的时间复杂度中,时间复杂度为O(logN)的算法十分高效,当数据规模增大n信时,耗时增大logN倍(这里的log以2为底)。例如,当数据增大256倍时,耗时增大8倍,因为2的8次方等于256。二分查找算法的时间复杂度为O(logN),每次查找排除掉一半的可能,1024个有序数据中最多查找10次就可以找到目标。
二分查找算法描述
用一维数组a存储有序元素序列,用变量low和high分别表示查找范围中第一个元素和最后一个元素的下标,mid表示查找范围的中间位置对应元素的下标,x为要查找的元素。
(1)变量初始化,令low=1,high=n。low和high分别初始化为有序序列的第一个元素和最后一个元素的下标。
(2)判断查找范围low≤high是否成立,如果成立,执行(3),否则输出"-1"(表示没有找到),结束算法。
(3)取中间元素,令mid=(low+high)/2,a[mid]就是中间元素。
(4)比较a[mid]与x,如果a[mid]等于x,则查找成功,结束算法;如果x<a[mid],则在序列的前半部分进行查找,修改查找的上界high=mid-1,下界不变,否则将在序列的后半部分进行在找,修改查找的下界low=mid+1,上界不变,转到(2)。
例如,从10个元素13,15,18,22,28,34,56,65,70.80中查找元素22的过程如图111-1所示最坏的情况下 4次可以找到。
特别注意:使用二分查找时,必须保证数据是有序的,若数据是无序的,则需要使用排序算法将数据变得有序。
二分查找算法的框架如下:
int ef(int a[],int n,int x)
{
int low=1,high=n,mid;
while(low<=high) //判断查找范围low<=high是否成立
{
mid=(low+high)/2; //取中间元素的位置
if(x==a[mid]) //x已经找到
{
return mid; //返回x对应的下标
}
else if(x<a[mid])
{
high=mid-1; //调整high,在前半部分查找
}
else low=mid+1; //调整low,在后半部分查找
}
return -1;
}
思考:为什么while循环的条件中是“<=”,而不是“<”?
例题1:查找m个数(主题库2646)
请你输入一个含 nn 个数字的不重复数列,请你高速的在这个数列中寻找 mm 个数字 x_1,x_2,...,x_mx1,x2,...,xm ,如果能找到直接输出,如果不存在输出 -1−1 ,用换行隔开(0<m<n<=10^60<m<n<=106)
输入格式:
输入共 4 行,第一行,为一个数字 nn 。第二行为 nn 个数字。第三行为一个数字 mm 。第四行为 mm 个要寻找的数字。
输出格式:
输出共 mm 行,每行一个数字,如果能找到直接输出原数字,如果找不到,输出 -1−1 。
样例输入
5
1 2 3 4 5
3
2 1 3
样例输出
2
1
3
问题分析:
解法一:暴力,样例没问题,提交0分。
#include<bits/stdc++.h>
using namespace std;
int a[1000001];
int main()
{
int n,m,i,j,t;
cin>>n;
for(i=1;i<=n;i++)
{
cin>>a[i];
}
cin>>m;
for(i=1;i<=m;i++)
{
cin>>t;
for(j=1;j<=n;j++)
{
if(a[j]==t)
{
cout<<j<<endl;
break;
}
}
}
return 0;
}
解法2:二分查找+递归
#include<bits/stdc++.h>
using namespace std;
int a[1000001];
int lower_bound(int x,int y,int t)
{
if(x>y){ return -1; }
int h=(x+y)/2;
if(a[h]==t) { return a[h]; }
if(a[h]>t) { y=h-1; return lower_bound(x,y,t); }
else { x=h+1; return lower_bound(x,y,t); }
}
int main()
{
int n,m,i,j,t;
cin>>n;
for(i=1;i<=n;i++) //一百万次
{ cin>>a[i]; }
sort(a+1,a+1+n); //n*log(n)2千万次
cin>>m;
for(i=1;i<=m;i++)
{
cin>>t;
cout<<lower_bound(1,n,t)<<endl;
}
return 0;
}
解法3:二分查找+递归
#include<bits/stdc++.h>
using namespace std;
int a[1000001];
int lower_bound(int x,int y,int t)
{
while(x<=y)
{
int h=(x+y)/2;
if(a[h]==t){ return a[h]; }
if(a[h]>t){ y=h-1; }
else{ x=h+1; }
}
return -1;
}
int main()
{
int n,m,i,j,t;
cin>>n;
for(i=1;i<=n;i++){ cin>>a[i]; }
sort(a+1,a+1+n); //n*log(n)2千万次
cin>>m;
for(i=1;i<=m;i++)
{
cin>>t;
cout<<lower_bound(1,n,t)<<endl;
}
return 0;
}
例题2:和为给定数(主题库2631)
数完正方形,大家决定休息一下,玩个游戏,首先由小天同学给出若干个整数,然后夏同学再给出一个,其它同学在小天同学的整数中查找其中是否有一对数的和等于夏同学给定的数。
输入格式:
共三行:
第一行是整数 n (0<n<=100,000)n(0<n<=100,000) ,表示有 nn 个整数。
第二行是 nn 个整数。整数的范围是在 00 到 100,000,000100,000,000 之间。
第三行是一个整数 m(0<=m<=2,000,000,000)m(0<=m<=2,000,000,000) ,表示需要得到的和。
输出格式:
若存在和为 mm 的数对,输出两个整数,小的在前,大的在后,中间用单个空格隔开。若有多个数对满足条件,选择数对中较小的数更小的。若找不到符合要求的数对,输出一行"No"。
样例输入:
4
2 5 1 4
6
样例输出:
15
数据范围/约定
20%的数据满足 0<n<=100,0<m<=20000<n<=100,0<m<=2000
50%的数据满足 0<n<=10,000,0<m<=20,000,0000<n<=10,000,0<m<=20,000,000
100%的数据满足 0<n<=100,000,0<m<=2,000,000,0000<n<=100,000,0<m<=2,000,000,000
问题分析:
解法1:有3个点超时
#include<bits/stdc++.h>
using namespace std;
int a[100001];
int main()
{
int n,i,j;
cin>>n;
long long m;
for(i=1;i<=n;i++)
{ cin>>a[i]; }
sort(a+1,a+1+n);
cin>>m;
if(m>200000000)
{
cout<<"No";
return 0;
}
for(i=1;i<=n;i++)
{
for(j=i+1;j<=n;j++)
{
if(a[i]+a[j]==m)
{
cout<<a[i]<<" "<<a[j];
return 0;
}
}
}
cout<<"No";
}
解法2:数组标记
#include<bits/stdc++.h>
using namespace std;
bool s[100000001];
int main()
{
int n,i;
cin>>n;
long long a,m;
for(i=1;i<=n;i++)
{
cin>>a; s[a]++;
}
cin>>m; //m最大是20亿
if(m>200000000)
{
cout<<"No";return 0;
}
for(i=0;i<=m/2;i++)
{
if(m-i<=100000000&&s[i]!=0&&s[m-i]!=0)
{
cout<<i<<" "<<m-i;
return 0;
}
}
cout<<"No";
}
解法3:二分查找
#include<bits/stdc++.h>
using namespace std;
int a[100001];
int lower_bound(int x,int y,int t)
{
while(x<=y)
{
int h=(x+y)/2;
if(a[h]==t){ return h; }
if(a[h]>t){ y=h-1; }
else { x=h+1; }
}
return -1;
}
int main()
{
int n,i,j; long long m;
cin>>n;
for(i=1;i<=n;i++){ cin>>a[i]; }
sort(a+1,a+1+n); cin>>m; //m最大是20亿
if(m>200000000) {cout<<"No";return 0;}
for(i=1;i<=n;i++)
{
if(m-a[i]<=100000000)//找一找 m-a[i] 是否存在
{
int k=m-a[i];
int h=lower_bound(i+1,n,k);
if(h!=-1)
{
cout<<a[i]<<" "<<a[h];
return 0;
}
}
}
cout<<"No";
return 0;
}
例题3:找朋友(主题库1189)
小学毕业后,同学们都进入了不同的初中,小明非常想念小伙伴们,所以他打算联系小学的同学们。 现在他得到了市内某所初中的所有名单,找出其中小明的小伙伴们。
输入输出格式
输入
第一行一个整数n,表示某初中人数。
接下来n行,每行一个字符串,只有小写字母组成,表示该校每个人的拼音。数据保证没有人拼音相同,且已经按照字典序从小到大排序。
第n+2行有一个整数m,表示小明的小伙伴个数。
最后m行,每行一个字符串,只有小写字母组成,表示每个小伙伴的拼音,同样保证没有重复。
输出
输出所有在该校的小伙伴的拼音。
每行一个拼音,顺序按照小伙伴给出的顺序。
样例输入:
3
alice
bob
zhangsan
2
lisi
zhangsan
样例输出:
zhangsan
数据范围
对于70%的数据,n<=1000,m<=100
对于100%的数据,n<=100000,m<=10000,每个人拼音长度不超过15。
所有数据,学校学生名单中的姓名,都是按照字典序从小到大排序。
问题分析:
#include<bits/stdc++.h>
using namespace std;
string a[100001];
int erfen(int x,int y,string t)
{
while(x<=y)
{
int h=(x+y)/2;
if(a[h]==t){ return h; }
if(a[h]>t){ y=h-1; }
else{ x=h+1; }
}
return -1;
}
int main()
{
int n,i,m; string t;
cin>>n;
for(i=1;i<=n;i++)
{ cin>>a[i]; }
cin>>m;
for(i=1;i<=m;i++)
{
cin>>t;
if(erfen(1,n,t)!=-1)
{ cout<<t<<endl; }
}
return 0;
}
例题4:伐树(主题库2644)
李老板需要总长为 MM 米的木材,他安排光头强去砍树。树林里有 NN 棵树,为了保护环境,不能将一个树完全砍掉,会留出一部分,因为这样树还可以继续生长。光头强将他的砍树装置的锯片高度设置为 HH 米,这样可以锯掉所有的树比 HH 高的部分。求在得到 MM 米木材的前提下, HH 的最大值。
比如,一共有 4 棵树,高度分别为 20、15、8、17,需要 6 米的木材,若将锯片的高度设置为 15 米,这样可以得到的木材为 5+0+0+2=7 米,若锯片的高度提高 1 米,设置为 16 米,只能得到木材的长度 4+1=5 。为了得到 6 米的木材,锯片的高度最大只能设置为 15 米。
输入格式:
第一行,两个整数 NN 和 MM 。
第二行,NN 个整数,表示每棵树的高度
输出格式:
一个整数,意义如上所述。
样例输入:
4 6
20 15 8 17
样例输出:
15
数据范围/约定
1<=N<=1000001<=N<=100000
1<=M<2^{31}1<=M<231
保证所有树的总长度不小于 MM
问题分析:
方法一:
#include <bits/stdc++.h>
using namespace std;
long long a[100001];
int main()
{
long long n,m,max=INT_MIN,t=INT_MIN;
cin>>n>>m;//需要总长为m的木材,树林里有n棵树
int i,j;
for(i=1;i<=n;i++)//输入这n棵树每棵树的高度
{
cin>>a[i];
if(a[i]>t)
{
t=a[i];
}
}
long long x=0,y=t,s=0;
long long h;
while(x<=y)
{
h=(x+y)/2;s=0;
for(i=1;i<=n;i++)
{
if(a[i]>h)
{
s=s+a[i]-h;
}
}
if(s>=m)//满足条件,找一个最大的h
{
if(h>max)
{
max=h;
}
x=h+1;
}
else
{
//不满足条件
y=h-1;
}
}
cout<<max;
}
方法二:
#include <iostream>
using namespace std;
long long a[1000050];
int main()
{
long long n,m,c,l=0,r=0;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
r=max(r,a[i]);
}
while(l<=r)
{
int mid=(l+r)/2;
c=0;
for(int i=1;i<=n;++i)
if(a[i]>mid) c+=a[i]-mid;
if(c<m) r=mid-1;
else l=mid+1;
}
cout<<r;
return 0;
}
例题5:眼红的 Medusa(题目来源∶洛谷P1571)
虽然Medusa领了科技创新奖,但是她还是觉得不满意。原因是,她发现很多人都和她一样获得了科技创新奖,特别是其中的某些人,还获得了另一个奖项——特殊贡献奖。而同时获得两个奖项的人越多,Medusa就越眼红。于是她决定统计有哪些人获得了两个奖项,来知道自己眼红的程度。
输入格式:
第1行,包含两个数n,m,表示有n个人获得科技创新奖,m个人获得特殊贡献奖第2行,包含n个正整数,表示获得科技创新奖的人的编号;第3行,包含m个正整数,表示获得特殊贡献奖的人的编号。
输出格式:
输出一行,为获得两个奖项的人的编号,按在科技创新奖获奖名单中的先后次序输出。
输入样例
4 3
2 15 6 8
8 9 2
输出样例
2 8
数据范围
对于60%的数据满足∶n≤100,m≤1000;
对于100%的数据满足∶n≤100000,m≤10000;
获得奖项的人的编号在2×10^9以内;
输入数据保证第2行任意两个数不同,第3行任意两个数不同。
问题分析:
题目要找到获得两个奖项的人的编号,并且都按照第1个获奖名单的先后次序进行输出。可以枚举第1个获奖名单中的每个编号,然后在第2个获奖名单中查找,若存在这个编号,则输出。这种算法的时间复杂度是O(n*n),考虑到n和m的范围,这种算法会超时。
在读入数据后可以先对第2个获奖名单中的编号进行排序,然后枚举第1个获奖名单中的每个编号,使用二分查找算法在第2个获奖名单中查找,将算法的时间复杂度降为O(nlogn)。
程序如下∶
#include<bits/stdc++.h>
using namespace std;
int a[100001],b[100010];
int ef(int a[],int n,int x) //二分查找,若找到,返回下标值,否则返回-1
{
int low=1,high=n,mid;
while(low<=high)
{
mid=(low+high)/2;
if(x==a[mid])
return mid;
else if(x<a[mid])
high=mid-1;
else
low=mid+1;
}
return -1;
}
int main()
{
int n,m,flag=1;
cin>>n>>m;
for(int i=1;i<=n;i++) //读入第1个获奖名单序列
cin>>a[i];
for(int i=1;i<=m;i++) //读入第2个获奖名单序列
cin>>b[i];
sort(b+1,b+m+1); //对第2个获奖名单进行升序排列
for(int i=1;i<=n;i++) //枚举第1个获奖名单中的每一个编号
{
if(ef(b,m,a[i])!=-1) //调用二分查找函数
{
cout<<a[i]<<" "; //只输出元素
}
}
cout<<endl;
return 0;
}
二分答案
二分答案算法思想
某些问题不容易直接求解,但却容易判断某个解是否可行,如果它的答案具有单调性(即如果答案x不可行,那么大于x的解都不可行面小于x的解有可能可行),就像上面的猜数游戏一样,我们可以根据题目的已知条件设定答案的上下界,然后用二分的方法枚举答案,再判断答案是否可行,根据判断的结果逐步缩小答案范围,直到找到符合题目条件的答案为止。
假设验证答案的时间复杂度为0(k),则解决整个问题的时间复杂度为0(klogn)。
二分答案算法描述
用变量low和high分别表示问题答案可能取值的下界和上界,我们用二分法假设一个可行解,用变量mid表示。用ans记录符合条件的解,用函数 check()检查解的合法性,判断猜测的变量mid的值是否符合条件,与题目中已知条件做比较,从而调整可行解的范围。假设求解符合条件的最大值,程序代码如下:
int BinSearch (int l, int r)
{
int ans, low=l, high=r;
whie(low<=high)
{//若下界<=上界,则继续二分答案
int mid=(low+high)>>1;//二分,有移1位表示除以2
if(check(mid))
{//若此答案可行
ans=mid;//记录可行解
low=mid+1;//从区间[mid+1,high]中继续查找(更大的答案)
}
else high=mid-1;//反之,若此答案不可行,从区间[1ow,mid-1中查找
}
return ans;//返回满足条件的最大解
}
思考:如果想找到符合条件的解的最小值,代码如何修改?
例题1:进击的奶牛(题目来源:洛谷P1824)
时间限制:1s,空间限制:125MB
Farmer John建造了一个有N个隔间的牛棚,这些隔间分布在一条数轴上(将隔间看成点,对应的数分别是x1,x2,…,xN。他的C头牛对隔间的位置分布不满,它们为牛棚里其他的牛的存在而愤怒。为了防止牛与牛之间的互相打斗, Farmer John想把这些牛安置在指定的隔间,所有牛中相邻两头的最近距离越大越好。那么,这个最大的最近距离是多少呢?
输入格式
第1行包含两个用空格隔开的数,分别表示N和C。
第2-(N+1)行,每行一个整数,表示每个隔间对应的数。
输出格式
行一个数,即相邻两头牛最大的最近距离。
输入样例:
5 3
1
2
8
4
9
输出样例:
3
【数据范围】
对于100%的数据满足:2<=N<=100000,2<=C<=N,0<=Xi<=10^9(i=1,2,···,N)。
问题分析
题目要计算相邻两头牛的最大的最近距离。由题意可知,牛必须安置在指定的隔间中如果允许,可以空出一些隔间来增大距离。采用二分答案的算法寻找最优解。
①确定下界与上界:用变量ans表示问题的解,则ans的取值范围是区间1,首尾隔间的距离];二分的下界low=1,上界high=首尾隔间的距离。
②确定二分策略:假设一个值为相邻两头牛最大的最近距离,用变量mid表示,判断二分得到的中间值mid是否是一个可行解,若是,则记录下当前的可行解,调整下界low,在区间[mid+1,high]内继续二分寻找可行解,直到low> high。
③可行解的判定:从所有隔间选择C个隔间安置牛,空闲的数量为N-C。安置方法如下:第一个隔间安排第一头牛,如果第二个隔间与第一个隔间的距离大于mid,则可以安置第二头牛,否则不能安置,即第二个隔间闲置,再去尝试第三个隔间安置第二头牛,依次类推。统计出闲置的隔间数量num,若num>N-C,则无法安排,此解不可行。若能安排完所有牛,则此解可行。
注意:输入的隔间位置并不是有序的,需要按从小到大进行排序。
程序如下:
#include<bits/stdc++.h>
using namespace std;
int a[1000010];
int n,c,res;
bool check(int x)
{
//判断此答案是否可行
int num=0; //num统计闲置的隔间数量
int pre=a[1];
//pre记录上一头牛的位置,开始时第一头牛在第一个隔间
for(int i=2;i<=n;i++)
{
//依次枚举每个隔间
if(a[i]-pre<x) num++;
//若两隔间之间的距离小于x,则此隔间不能使用,闲置的隔间数+1
else pre=a[i];//否则使用此隔间,更新pre
if(num>n-c) return false;//若闲置的隔间数超过n-c,此答案不可行
}
return true;//若需要隔间数小于最大需要隔间数,此答案可行
}
int Binsearch(int l,int r)
{
//二分答案
int ans, low=l, high=r;
while(low<=high)
{
//若下界<=上界,则继续二分答案
int mid=(low+high)>>1;//二分
if(check(mid))
{
//若此答案可行
ans=mid;//记录可行解
low=mid+1;//在区间[mid+1,high]中继续查找(更大答案)
}
else high=mid-1;//若此答案不可行,在区间[1ow,mid-1]中查找
}
return ans;//返回满足条件的最大解
}
int main()
{
cin>>n>>c;
for (int i=1;i<=n; i++)
cin>>a[i];
sort(a+1,a+n+1); //由于无序,需按从小到大排序
res=Binsearch(1,a[n]-a[1]);//二分答案
cout<<res<<endl;//输出结果
return 0;
}
例题2:月度开销(题目来源OpenJudge 1.11.06)
农夫约翰是一个精明的会计师。他意识到自己可能没有足够的钱来维持农场的运转了。他计算出并记录下了接下来 N (1 ≤ N ≤ 100,000) 天里每天需要的开销。
约翰打算为连续的M (1 ≤ M ≤ N) 个财政周期创建预算案,他把一个财政周期命名为fajo月。每个fajo月包含一天或连续的多天,每天被恰好包含在一个fajo月里。
约翰的目标是合理安排每个fajo月包含的天数,使得开销最多的fajo月的开销尽可能少。
输入
第一行包含两个整数N,M,用单个空格隔开。
接下来N行,每行包含一个1到10000之间的整数,按顺序给出接下来N天里每天的开销。
输出
一个整数,即最大月度开销的最小值。
样例输入
7 5
100
400
300
100
500
101
400
样例输出
500
解题思路:若约翰将前两天作为一个月,第三、四两天作为一个月,最后三天每天作为一个月,则最大月度开销为500。其他任何分配方案都会比这个值更大。
二分答案典型问法,最大中最小,首先确定二分范围,左端点是n天中的最大值(因为题目中说是最大月度),右端点是全部总和(可以极限的想他们全在一个月份里)
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
using namespace std;
int n,m,a[100009],maxx=0,tot,ans;
bool check(int);
int main()
{
scanf("%d%d",&n,&m);//输入n天,m个财政周期
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
tot+=a[i];//二分的右端点是总的开销
if(a[i]>maxx)maxx=a[i];//二分的左端点是n天中最大的开销
}
int l=maxx,r=tot,mid;
while(l<=r)//二分查找
{
mid=(l+r)>>1;
if(check(mid))//如果中间值可以
{
ans=mid;//记录答案
r=mid-1;//缩小范围
}
else
l=mid+1;//否则扩大范围
}
printf("%d",ans);//输出最小值
}
bool check(int x)//当最小值是x时是否可以
{
int sum=0,yfen=1;
for(int i=1;i<=n;i++)
{
if(sum+a[i]>x)//sum是一直累加,当超过x时月份++,说明这一天自己另一个月份
{
sum=a[i];
yfen++;
}
else
sum+=a[i];
}
if(yfen<=m)return 1;//月份等于m时正好分成m个月份,小于m时,可以将某些月份中的天数拆开组成新月份,满足分成m个月份
else
return 0;
}
二分答案总结
二分答案常被用来求解最小值最大或最大值最小等最值问题,将最优化问题转换为判定问题。
适用条件:
单调性∶ 问题的答案具有单调性。
枚举可求解∶ 问题的答案可以通过枚举求解,可以将二分答案理解为枚举的一种优化。
基本步骤:
确定答案范围∶ 确定问题答案可能的的最小值和最大值。
确定上下界∶ 确定问题是求上界还是求下界。
判定方案∶确定判定方案,编写判定函数(check函数)。
二分查找与二分答案:高效算法解析与实例
本文详细介绍了二分查找算法,包括其时间复杂度、算法描述和实例应用,强调了其在有序数据中的高效性。此外,文章还探讨了二分答案算法的思想,给出了寻找最小值的二分答案策略,并通过具体例题说明了如何应用二分答案解决实际问题。二分查找与二分答案在解决最值问题时表现出高效性和实用性。
3233

被折叠的 条评论
为什么被折叠?



