算法训练营 搜索技术(二分搜索)

二分搜索

原理 二分搜索技术

  • 给定有n个元素的序列,这些元素是有序的(假定为升序),从序列中查找元素x
  • 用一维数组S[]存储该有序序列,设变量lowhigh表示查找范围的下界和上界,middle表示查找范围的中间位置,x表示特定的查找元素

算法设计

  1. 初始化。令low = 0,即指向有序数组S[]的第1个元素;high = n-1,即指向有序数组S[]的最后一个元素。
  2. 判定 l o w ≤ h i g h low \leq high lowhigh是否成立,如果成立,则转向步骤3,否则算法结束。
  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+(highlow)/2
  4. 判断xS[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。如果lr特别大,则为了避免l+r溢出,可以采用mid = l+(r-l)/2。对判断二分搜索结束的条件以及判断mid可行时是在前半部分搜索还是在后半部分搜索,需要根据具体问题分析。
  • 在减少搜索范围时,要注意是否漏掉了mid点上的答案。

训练1:跳房子游戏

跳房子游戏

跳房子游戏指从河中的一块石头跳到另一块石头,这发生在一条又长又直的河流中,从一块石头开始,到另一块石头结束。长度为 L L L 1 ≤ L ≤ 1 0 9 1 \leq L \leq 10^{9} 1L109),从开始到结束之间的石头数量为 N N N 0 ≤ N ≤ 50000 0 \leq N \leq 50000 0N50000),从每块石头到开始位置有一个整数距离 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 0MN)。请确定在移除 M M M块石头后,母牛必须跳跃的最短距离的最大值。

输出:第1行包含3个整数 L L L N N N M M M。接下来的 N N N行,每行都包含一个整数,表示从该石头到起始石头的距离。没有两块石头有相同的位置。

输出:单行输出移除 M M M块石头后母牛必须跳跃的最短距离的最大值。

算法设计

  1. 如果移除的石头数等于总石头数( M = N M = N M=N),则直接输出 L L L
  2. 增加开始(0)和结束( N + 1 N+1 N+1)两块石头,从开始节点的距离分别为0和 L L L
  3. 对所有的石头都按照到开始节点的距离从小到大排序。
  4. 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 rightleft>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,继续二分搜索。
  5. 搜索结束后, 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} 1n105);第2行包含 a i a_{i} ai 1 ≤ a i ≤ 1 0 9 1 \leq a_{i} \leq 10^{9} 1ai109, 1 ≤ i ≤ n 1 \leq i \leq n 1in

输出:单行输出烘干所有衣服所需的最少时间

算法设计

  • 假设烘干所有衣服所需的最少时间为 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 midt选择自然风干,那么 a [ i ] = k ∗ t + m i d − t a[i] = k* t + mid -t a[i]=kt+midt t = ( a [ i ] − m i d ) / ( k − 1 ) t = (a[i] - mid)/(k-1) t=(a[i]mid)/(k1)。只需判断这些 a [ i ] a[i] a[i]大于 m i d mid mid的衣服使用烘干机的总时间有没有超过 m i d mid mid,如果超过则不满足条件
  1. 按照 a i a_{i} ai从小到大排序
  2. 如果 k = 1 k = 1 k=1,则直接输出 a [ n − 1 ] a[n-1] a[n1],算法结束。
  3. 进行二分搜索, l = 1 l = 1 l=1 r = a [ n − 1 ] r = a[n-1] r=a[n1] 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=mid1,减少时间继续搜索;否则 l = m i d + 1 l = mid+1 l=mid+1,增加时间继续搜索。当 l > r l>r l>r时停止。
  4. 判断最少烘干时间为 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。
  5. 注意:对 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(3N1000),表示花环中等的数量, A ( 10 ≤ A ≤ 1000 ) A(10 \leq A \leq 1000) A(10A1000)表示地面上最左边的灯的高度(实数,以毫米为单位)

输出:单行输出B,精确到小数点右边两位数,表示最右边灯的最低可能高度

算法设计

  1. 二分搜索。初始时,num[1] = Al = 0.0r = inf(即:0x3f3f3f3f),mid = (l+r)/2,判断第2个灯的高度为mid是否可行,如果可行,则令r = mid,缩小高度搜索;否则l = mid,增加高度搜索。
  2. 利用公式倒退判断mid是否可行。
  3. 可以用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)

算法设计

  1. 二分搜索。初始时,l = 0.0r = inf r r r也可以被初始化为 N N N条电缆中的最大长度。mid = (l+r)/2,判断切割出来电联的长度为mid,是否可以切割K条,如果可以,则令l = mid,增加长度搜索,否则r = mid,减少长度搜索。
  2. 判断mid是否可行。枚举N条电缆,累加每条电缆可以切割出的数量,注意该数量要取整(int)(L[i]/mid),如果数量大于或等于K,则表示可行。
  3. 可以用r-l>eps判断循环条件,也可以在搜索较大的次数时停止。
  4. 输出答案。本题要求保留两位小数,切割后不可四舍五入,因此可以扩大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
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

羽星_s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值