二分搜索
原理 二分搜索技术
- 给定有
n
个元素的序列,这些元素是有序的(假定为升序),从序列中查找元素x
。 - 用一维数组
S[]
存储该有序序列,设变量low
和high
表示查找范围的下界和上界,middle
表示查找范围的中间位置,x
表示特定的查找元素
算法设计
- 初始化。令
low = 0
,即指向有序数组S[]
的第1个元素;high = n-1
,即指向有序数组S[]
的最后一个元素。 - 判定 l o w ≤ h i g h low \leq high low≤high是否成立,如果成立,则转向步骤3,否则算法结束。
- m i d d l e = ( l o w + h i g h ) / 2 middle= (low+high)/2 middle=(low+high)/2,即指向查找范围的中间元素。如果数量较大,则为避免 l o w + h i g h low+high low+high溢出,可以采用 m i d d l e = l o w + ( h i g h − l o w ) / 2 middle = low+(high-low)/2 middle=low+(high−low)/2。
- 判断
x
与S[middle]
的关系。如果x = S[middle]
,则搜索成功,算法结束;如果x > S[middle
],则令low = middle+1
;否则令high = middle - 1
,转向步骤2
算法实现
#include<iostream>
#include<algorithm>
using namespace std;
const int M=100;
int x,n,i;
int s[M];
int BinarySearch(int s[],int n,int x); //二分查找非递归算法
int main(){
cout<<"该数列中的元素个数n为:";
cin>>n;
cout<<"请依次输入数列中的元素:";
for(i=0;i<n;i++)
cin>>s[i];
sort(s,s+n); //二分查找的序列必须是有序的,如果无序需要先排序
cout<<"排序后的数组为:";
for(i=0;i<n;i++)
cout<<s[i]<<" ";
cout<<endl;
cout<<"请输入要查找的元素:";
cin>>x;
i=BinarySearch(s,n,x);
if(i==-1)
cout<<"该数列中没有要查找的元素"<<endl;
else
cout<<"要查找的元素在第"<<i+1<<"位"<<endl;//位序和下标差1
return 0;
}
int BinarySearch(int s[],int n,int x){//二分查找非递归算法
int low=0,high=n-1; //low指向有序数组的第一个元素,high指向有序数组的最后一个元素
while(low<=high){
int middle=(low+high)/2; //middle为查找范围的中间值
if(x==s[middle]) //x等于查找范围的中间值,算法结束
return middle;
else if(x>s[middle]) //x大于查找范围的中间元素,则从左半部分查找
low=middle+1;
else //x小于查找范围的中间元素,则从右半部分查找
high=middle-1;
}
return -1;
}
输入与输出:
该数列中的元素个数n为:7
请依次输入数列中的元素:5 8 15 17 25 30 34
排序后的数组为:5 8 15 17 25 30 34
请输入要查找的元素:17
要查找的元素在第4位
二分搜索条件
- 使用二分搜索必须满足有序性。
- 搜索范围。初始时需要指定搜索范围,如果不知道具体范围,则对正数可以采用范围
[0,inf]
,对负数可以采用范围[-inf,inf]
,inf
为无穷大,通常设定为0x3f3f3f3f
- 二分搜索。在一般情况下
mid = (l+r)/2
。如果l
和r
特别大,则为了避免l+r
溢出,可以采用mid = l+(r-l)/2
。对判断二分搜索结束的条件以及判断mid
可行时是在前半部分搜索还是在后半部分搜索,需要根据具体问题分析。 - 在减少搜索范围时,要注意是否漏掉了
mid
点上的答案。
训练1:跳房子游戏
跳房子游戏
跳房子游戏指从河中的一块石头跳到另一块石头,这发生在一条又长又直的河流中,从一块石头开始,到另一块石头结束。长度为 L L L( 1 ≤ L ≤ 1 0 9 1 \leq L \leq 10^{9} 1≤L≤109),从开始到结束之间的石头数量为 N N N( 0 ≤ N ≤ 50000 0 \leq N \leq 50000 0≤N≤50000),从每块石头到开始位置有一个整数距离 d 1 d_{1} d1( 0 < d t < L 0 < d_{t} < L 0<dt<L)
为了玩游戏,每头母牛都依次从起始石头开始,并尝试到达终点的石头,只能从石头跳到石头。当然,不那么灵活的母牛永远不会到达最后的石头,而是掉进河里。约翰计划移除几块石头,以增加母牛必须跳到最后的最短距离。不能删除起点和终点的石头,但约翰有足够的资源移除多达 M M M块石头( 0 ≤ M ≤ N 0 \leq M \leq N 0≤M≤N)。请确定在移除 M M M块石头后,母牛必须跳跃的最短距离的最大值。
输出:第1行包含3个整数 L L L、 N N N和 M M M。接下来的 N N N行,每行都包含一个整数,表示从该石头到起始石头的距离。没有两块石头有相同的位置。
输出:单行输出移除 M M M块石头后母牛必须跳跃的最短距离的最大值。
算法设计
- 如果移除的石头数等于总石头数( M = N M = N M=N),则直接输出 L L L
- 增加开始(0)和结束( N + 1 N+1 N+1)两块石头,从开始节点的距离分别为0和 L L L。
- 对所有的石头都按照到开始节点的距离从小到大排序。
- 令 l e f t = 0 left = 0 left=0, r i g h t = L right = L right=L,如果 r i g h t − l e f t > 1 right - left>1 right−left>1,则 m i d = ( r i g h t + l e f t ) / 2 mid = (right + left)/2 mid=(right+left)/2,判断是否满足移除 M M M块石头之后,任意间距都不小于 m i d mid mid,如果满足,则说明距离还可以更大,令 l e f t = m i d left = mid left=mid;否则令 r i g h t = m i d right = mid right=mid,继续二分搜索。
- 搜索结束后, l e f t left left就是母牛必须跳跃的最短距离的最大值。
算法实现
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=50050;
int L,n,m,dis[maxn];
bool judge(int x); //移除m个石头之后,任意间距不小于x
int main(){
cin>>L>>n>>m;
if(n==m){
cout<<L<<endl;
return 0;
}
for(int i=1;i<=n;i++)
cin>>dis[i];
dis[0]=0;//增加开始点
dis[n+1]=L;//增加结束点
sort(dis,dis+n+2);
int left=0,right=L;
while(right-left>1){
int mid=(right+left)/2;
if(judge(mid))
left=mid;//如果放得开,说明x还可以更大
else
right=mid;
}
cout<<left<<endl;
return 0;
}
bool judge(int x){ //移除m个石头之后,任意间距不小于x
int num=n-m; //减掉m个石头,放置num个石头,循环num次
int last=0; //记录前一个已放置石头下标
for(int i=0;i<num;i++){ //对于这些石头,要使任意间距不小于x
int cur=last+1;
while(cur<=n&&dis[cur]-dis[last]<x) //放在第1个与last距离>=x的位置
cur++; //由cur累计位置
if(cur>n)
return 0; //如果在这个过程中大于n了,说明放不开
last=cur; //更新last位置
}
return 1;
}
输入:
25 5 2
2
14
11
21
17
输出:
4
训练2:烘干衣服
题目描述
可以使用散热器烘干衣服。但散热器很小,所以它一次只能容纳一件衣服。简有n件衣服,每件衣服在洗涤过程中都带有 a i a_{i} ai的水,在自然风干的情况下,每件衣服的含水量每分钟减少1(只有当物品还没有完全干燥时)。当含水量变为零时,布料变干并准备好包装。在散热器上烘干时,衣服的含水量每分钟减少 k k k(如果衣服含有少于 k k k的水,则衣服的含水量变为零)。请有效地使用散热器来最小化烘干的最小化烘干的总时间
输入:第1行包含一个整数 n n n( 1 ≤ n ≤ 1 0 5 1 \leq n \leq 10^{5} 1≤n≤105);第2行包含 a i a_{i} ai( 1 ≤ a i ≤ 1 0 9 1 \leq a_{i} \leq 10^{9} 1≤ai≤109, 1 ≤ i ≤ n 1 \leq i \leq n 1≤i≤n)
输出:单行输出烘干所有衣服所需的最少时间
算法设计
- 假设烘干所有衣服所需的最少时间为 m i d mid mid,如果所有衣服的含水量 a [ i ] a[i] a[i]都小于 m i d mid mid,则不需要用烘干机,自然烘干的时间也不会超过 m i d mid mid。如果有的衣服 a [ i ] a[i] a[i]大于 m i d mid mid,则让所有 a [ i ] a[i] a[i]大于 m i d mid mid的衣服使用烘干机,让 a [ i ] a[i] a[i]不大于 m i d mid mid的衣服自然风干即可
- 假设衣服 a [ i ] > m i d a[i] > mid a[i]>mid,用了 t t t时间的烘干机,对剩余的时间 m i d − t mid-t mid−t选择自然风干,那么 a [ i ] = k ∗ t + m i d − t a[i] = k* t + mid -t a[i]=k∗t+mid−t, t = ( a [ i ] − m i d ) / ( k − 1 ) t = (a[i] - mid)/(k-1) t=(a[i]−mid)/(k−1)。只需判断这些 a [ i ] a[i] a[i]大于 m i d mid mid的衣服使用烘干机的总时间有没有超过 m i d mid mid,如果超过则不满足条件
- 按照 a i a_{i} ai从小到大排序
- 如果 k = 1 k = 1 k=1,则直接输出 a [ n − 1 ] a[n-1] a[n−1],算法结束。
- 进行二分搜索, l = 1 l = 1 l=1, r = a [ n − 1 ] r = a[n-1] r=a[n−1], m i d = ( l + r ) / 2 mid = (l+r)/2 mid=(l+r)/2,判断最少烘干时间为 m i d mid mid是否可行,如果可行,则 r = m i d − 1 r = mid-1 r=mid−1,减少时间继续搜索;否则 l = m i d + 1 l = mid+1 l=mid+1,增加时间继续搜索。当 l > r l>r l>r时停止。
- 判断最少烘干时间为 m i d mid mid是否可行。对所有 a [ i ] > m i d a[i]>mid a[i]>mid的衣服使用烘干机,用 s u m sum sum累加使用烘干机的时间,如果 s u m > m i d sum>mid sum>mid,则说明不可行,返回0。当所有衣服都处理完毕时,返回1。
- 注意:对 t t t的结果需要向上取整,因为如果有余数,再用一次烘干机无非就是多1个时间,但是如果自然风干,则至少用1个时间
算法实现
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
int n,k;
int a[100005];
int judge(int x);
void solve();
int main(){
while(~scanf("%d",&n)){
for(int i=0;i<n;i++)
scanf("%d",&a[i]);
scanf("%d",&k);
sort(a,a+n);
if(k==1){
printf("%d\n",a[n-1]);
continue;
}
solve();
}
return 0;
}
int judge(int x){
int sum=0;
for(int i=0;i<n;i++){
if(a[i]>x)
sum+=(a[i]-x+k-2)/(k-1);//上取整,或 ceil((a[i]-x)*1.0/(k-1));
if(sum>x)
return 0;
}
return 1;
}
void solve(){
int l=1,r=a[n-1],ans;
while(l<=r){
int mid=(l+r)>>1; //mid = (l+r)/2
if(judge(mid)){
ans=mid;
r=mid-1;//减小
}
else
l=mid+1;//增大
}
cout<<ans<<endl;
}
输入:
3
2 3 9
5
3
2 3 6
5
输出:
3
2
训练3:花环
题目描述
新年花环由N个灯组成,每个灯都悬挂在比两个相邻灯的平均高度低1毫米的高度处。最左边的灯挂在地面以上A毫米的高度处。必须确定最右侧的最低高度B,以便花环中的灯不会落在地面上,尽管其中一些灯可能会接触地面。灯的编号为1 ~ N,并以毫米为单位表示第i个灯的高度为 H i H_{i} Hi。
输入:输入包含两个数字 N N N和 A A A。 N ( 3 ≤ N ≤ 1000 ) N(3 \leq N \leq 1000) N(3≤N≤1000),表示花环中等的数量, A ( 10 ≤ A ≤ 1000 ) A(10 \leq A \leq 1000) A(10≤A≤1000)表示地面上最左边的灯的高度(实数,以毫米为单位)
输出:单行输出B,精确到小数点右边两位数,表示最右边灯的最低可能高度
算法设计
- 二分搜索。初始时,
num[1] = A
,l = 0.0
,r = inf
(即:0x3f3f3f3f),mid = (l+r)/2
,判断第2个灯的高度为mid
是否可行,如果可行,则令r = mid
,缩小高度搜索;否则l = mid
,增加高度搜索。 - 利用公式倒退判断mid是否可行。
- 可以用r-l>eps判断条件,也可以搜索到较大的次数时停止,为保险起见,尽量执行较多次数,如100次。
算法实现
#include<iostream>
#include<cstdio>
using namespace std;
#define inf 0x3f3f3f3f
#define maxn 1006
#define eps 1e-7
int n;
double A,num[maxn],ans;
bool check(double mid); //判断第二个灯的高度为mid是否可行
void solve();
int main(){
while(~scanf("%d%lf",&n,&A)){
solve();
printf("%.2lf\n",ans);
}
return 0;
}
bool check(double mid){//判断第二个灯的高度为mid是否可行
num[2]=mid;
for(int i=3;i<=n;i++){
num[i]=2*num[i-1]-num[i-2]+2;
if(num[i]<eps) return false; //写小于0由于精度问题会wa
}
ans=num[n];
return true;
}
void solve(){
num[1]=A;
double l=0.0;
double r=A;//inf
while(r-l>eps){//for(int i=0;i<100;i++)
double mid=(l+r)/2;
if(check(mid))
r=mid;
else
l=mid;
}
}
输入:
692 532.81
输出:
446113.34
训练4:电缆切割
题目描述
有N条电缆,长度分别为L_{i},如何从它们中切割出K条长度相同的电缆,每条电缆最长有多少米。
输入:输入的第1行包含两个整数N和K(1 \leq N,K \leq 10000)。N是电缆的数量,K是要求切割的数量。后面是N行,每行一个数字L_{i}(1 \leq L_{i} \leq 100000),表示每条电缆的长度
输出:单行输出电缆切割的最大长度(在小数点后保留两位数字。如果不能切割所要求数量的电缆,则输出0.00)
算法设计
- 二分搜索。初始时,
l = 0.0
,r = inf
, r r r也可以被初始化为 N N N条电缆中的最大长度。mid = (l+r)/2
,判断切割出来电联的长度为mid
,是否可以切割K
条,如果可以,则令l = mid
,增加长度搜索,否则r = mid
,减少长度搜索。 - 判断
mid
是否可行。枚举N
条电缆,累加每条电缆可以切割出的数量,注意该数量要取整(int)(L[i]/mid)
,如果数量大于或等于K
,则表示可行。 - 可以用
r-l>eps
判断循环条件,也可以在搜索较大的次数时停止。 - 输出答案。本题要求保留两位小数,切割后不可四舍五入,因此可以扩大100倍取下限,然后缩小100倍,舍去2位小数之后的数字,但是存在特殊情况,例如1.59999999,这样的数近似于1.60,可以加上一个特别小的数处理该问题,因此返回答案
ans
加上eps即可。
算法实现
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
#define inf 0x3f3f3f3f
#define eps 1e-7
double L[10005];
int n,k;
bool judge(double x){//假设切割出来的绳子的长度为x,判断够不够切割
int num=0;
for(int i=0;i<n;i++)
num+=(int)(L[i]/x);
return num>=k;
}
double solve(){
double l=0;
double r=*(max_element(L,L+n));//inf;
while(r-l>eps){//for(int i=0;i<100;i++){
double mid=(l+r)/2;
if(judge(mid))
l=mid;
else
r=mid;
}
return l;
}
int main(){
while(~scanf("%d%d",&n,&k)){
for(int i=0;i<n;i++)
scanf("%lf",&L[i]);
double ans=solve()+eps;//也可以在solve()中直接返回r,循环结束时r比l多eps
printf("%.2lf\n", floor(ans*100)/100); //取下限,或int(ans*100)/100.0
}
return 0;
}
输入:
4.11
8.02
7.43
4.57
5.39
输出:
2.00