文章目录
注:本系列文章均分为新手与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
n≤1000,1≤k,ai≤105
这是一道典型的二分题,虽然暴力也能过就是了。
如果使用暴力,很明显,时间会拖得非常长,最劣的情况为
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)
但二分同时也有许多限制,就比如刚刚那道题,我们可以发现:
- 二分时需要保证数据单调递增(递减)。
- 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=1∑k(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—