寒假一期:算法(1):二分


注:本系列文章均分为新手与EX两部分,如果是新手,建议先学习理论部分,有基础的建议在EX进行巩固

新手

引入:二分查找

二分,从字面意思上便可以理解,便是把一个整体平分为两个部分,再继续缩小范围。
很明显,这是一个非常便利的算法,就比如下面

例题

求一个长度为 n n n 的单调递增的数组 a a a 中小于等于 k k k 的最后一个数的位置
n ≤ 1000 , 1 ≤ k , a i ≤ 1 0 5 n\le1000,1\le k,a_i\le10^5 n1000,1k,ai105
这是一道典型的二分题,虽然暴力也能过就是了。
如果使用暴力,很明显,时间会拖得非常长,最劣的情况为 O ( k ) O(k) O(k)
而使用二分,则只需要进行 log ⁡ 2 n \log_2n log2n 次的分割,哪怕 n = 1000 n=1000 n=1000 也仅仅循环九次。
代码放在下方,读者可以自行体会其中的差距;

暴力
#include<bits/stdc++.h>
using namespace std;
int a[1005],n,k;
int main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		if(a[i]>k){
			cout<<i-1;
			return 0;
		}
	}
}
二分
#include<bits/stdc++.h>
using namespace std;
int a[1005],n,k;
int main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	int l=1,r=n,mid,ans=1;
	while(l<=r){
		mid=(l+r)/2;
		if(a[mid]<=k){
			ans=mid;
			l=mid+1;
		}else{
			r=mid-1;
		}
	}
	cout<<ans;
}

时间上的差异应该很明显便能对比出来。
不难发现,虽然二分的时间复杂度很优,仅有 O ( log ⁡ 2 n ) O(\log_2n) O(log2n)
但二分同时也有许多限制,就比如刚刚那道题,我们可以发现:

  1. 二分时需要保证数据单调递增(递减)。
  2. l l l r r r 十分依靠范围,如果范围不明确,很有可能WA。

以上两点是初学者非常需要注意的。
对二分的使用不当很可能造成对程序毁灭性的错误。

lower_bound与upper_bound函数

这两个函数便是用来二分查找的,妈妈再也不用担心我懒得写二分了
lower_bound 用来查找第一个大于等于。
upper_bound 则查找第一个大于。
使用方法如下:

int a[1005];
lower_bound(a,a+k,l);//从a[0]到a[k],查找第一个大于等于l的数
upper_bound(a,a+k,l);//从a[0]到a[k],查找第一个大于l的数

当然,也可以改变参数,反向查找

int a[1005];
lower_bound(a,a+k,l,greater<int>());//从a[0]到a[k],查找第一个小于等于l的数
upper_bound(a,a+k,l,greater<int>());//从a[0]到a[k],查找第一个小于l的数

如上面的代码,便可改成

#include<bits/stdc++.h>
using namespace std;
int a[105];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
    	cin>>a[i];
	}
	int k;
	cin>>k;
	int ans=lower_bound(a+1,a+n+1,k)-a;//因为函数为返回其存储位置,所以减去a数组起始位置
	cout<<ans;
}

习题

查找

查找——题目
题目已经说得非常详细了,这就是一道二分模板题,所以照着做就行了。

AC code
#include<bits/stdc++.h>
using namespace std;
int a[1000005];
int main(){
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++){
	    cin>>a[i]
	}
	for(int i=1;i<=m;i++){
	    int q;
	    cin>>q;
	    int l=1,r=n;
	    while(l<r){
	        int mid=(r+l)>>1;
	        if(a[mid]>=q){
	            r=mid;
	        }else{
	            l=mid+1;
	        }
	    }
	    if(a[l]==q){
	        cout<<l<<" ";
	    }else{
	        cout<<-1<<" ";
	    }
	}
}
烦恼的高考志愿

烦恼的高考志愿——题目
一道考察排序与二分的题
主要是要去比较估分与前后两所学校的差距,取更小的一个。
技巧:熟练使用 sort 与 lower_bound 函数

AC code
#include<bits/stdc++.h> 
using namespace std;
long long n,m,ans;
long long a[100005],b[100005];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int i=1;i<=m;i++){
		cin>>b[i];
	}
	sort(a+1,a+n+1);
	for(int i=1;i<=m;i++){
		int tmp=lower_bound(a+1,a+n+1,b[i])-a;
		if(tmp==n+1)	ans+=b[i]-a[n];
		else if(tmp==1)	ans+=a[1]-b[i];
		else 	ans+=min(abs(a[tmp]-b[i]),abs(b[i]-a[tmp-1]));
	}
	cout<<ans;
}

二分答案

很好,现在已经进入第二个部分——二分答案了。
顾名思义,二分答案,就是对答案进行二分。这不是废话吗
很明显,既然是对答案二分,那么肯定跟二分查找不同。
通常,答案需要满足某个性质,使得这个性质对 mid 应有的答案单调。
比如,一个函数。

习题

银行贷款

银行贷款——题目
根据利率公式,设本金为 n n n ,利率为 m m m ,每月还的钱为 s s s , k k k为还债的时间
因为是利滚利
∴ ∑ i = 1 k s ( 1 + m ) i = n \therefore \sum_{i=1}^{k}{\frac{s}{(1+m)^i}}=n i=1k(1+m)is=n
然后就可以写出程序

AC code
#include<bits/stdc++.h>
using namespace std;
double n,m,k;
double L,R;
inline bool f(double x){
	double s=0,ss=1;
	for(int i=1;i<=k;i++){
		ss*=(1+x);
		s+=m/ss;
	}
	return s>=n;//如果大于等于,说明利率小了(这不是显而易见吗)
}
int main(){
	cin>>n>>m>>k;
	L=0,R=5;
	while(R-L>1e-5){
		double mid=(L+R)/2;
		if(f(mid)){
			L=mid;//增大mid
		}else{
			R=mid;
		}
	}
	cout<<fixed<<setprecision(1)<<L*100.0;
}

有的人可能已经注意到了,在实数域上的二分与在整数域上不同,没有加加减减,退出循环靠一个精度,并且没有 ans 记录答案,可以直接输出 L 或 R 。
自此,二分的初步学习到此为止。


EX

开始深入递归

EX.1 寻找伪币

在这里插入图片描述

样例输入
2 2 2 1 2 2 2 2 2 2 2 2 2 2 2 2
样例输出
4 1

二分出中点,左右比对大小,递归继续比较,可以使用前缀和优化

AC code
#include<bits/stdc++.h>
using namespace std;
int sum[17],dp[17];
int dfs(int l,int r){
	if(l==r)	return l+1;
	int mid=(l+r)/2;
	if(dp[mid]-dp[l]<dp[r]-dp[mid])	return dfs(l,mid);//如果前半段重量和小,递归前半段。
	return dfs(mid,r);//递归后半段
}
int main(){
	for(int i=1;i<=16;i++){
		cin>>sum[i];
		dp[i]=dp[i-1]+sum[i];//前缀和优化
	}
	cout<<dfs(0,16)<<" "<<sum[dfs(0,16)];
}

复杂的函数

EX.2 跳房子

跳房子——题目
仍然是在整数与上的二分,不过函数内部需要进行 DP ,以至于实现二分的函数非常复杂,很难想到。思维难度很高。

AC code
#include<bits/stdc++.h>
using namespace std;
long long n,d,k;
long long a[500005],s[500005];
long long dp[500005];
bool f(long long x){
	long long smin=max(1,d-x),smax=d+x;//记录最小与最大跳跃距离。
	memset(dp,-127,sizeof(dp));
	dp[0]=0;
	for(long long i=1;i<=n;i++){
		for(long long j=i-1;j>=0;j--){//枚举可以跳跃到第i个格子的格子
			if(a[i]-a[j]<smin)	continue;//如果格子间的距离小于最小跳跃距离,跳过
			if(a[i]-a[j]>smax)	break;//如果格子间的距离大于最大跳跃距离,因为越往前,格子的距离越大,所以前面不可能有能跳到这个格子的格子了,退出循环。
			dp[i]=max(dp[i],dp[j]+s[i]);
			if(dp[i]>=k)	return 1;//满足要求,返回true
		}
	}
	return 0;
}
int main(){
	long long ans=-1,l=1,r=1005;
	cin>>n>>d>>k;
	for(long long i=1;i<=n;i++){
		cin>>a[i]>>s[i];
	}
	while(l<=r){
		long long mid=(l+r)/2;
		if(f(mid)){
			ans=mid;
			r=mid-1;
		}else{
			l=mid+1;
		}
	}//整数二分
	cout<<ans;
}

EX.3

如果你真的认真学习了二分,那么下面这道题就是来练手的。
路标设置
提示:枚举“空旷指数”,函数计算路灯个数,进行二分。

别人那里得来的知识,永远不如自己消化来得好,别人的代码永远只是一个借鉴,我讲的也只有模板与理论,要想真正学会,必须动手实践。
加油!!!

—end—

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值