作者上次忘搞专栏了,所以重新搞了一次(狗头见谅)
写在前面
Hi,小伙伴们,I am coming🌹。算法是一门必修课,也是计算机程序的基础。神犇们虽说已是轻舟已过万重山,但也可以留下来回忆回忆,重温一下,提供宝贵的经验与见解🌹🌹。还在学习算法的小伙伴们也别急,静下心来,学习算法,扩宽思维,体会算法的“优美”🌹。
/*想看题解的小伙伴直接点击目录,选择题目即可*/
/*这篇题解只讲思路,不会太详细,但该讲的都会讲,我相信小伙伴都有实力,实力强大👍👍*/
二分
二分可以分为 二分查找&&二分答案。
二分查找是对问题的一种快速搜索,容易理解,主要作用——大大降低时间复杂度。
而二分答案呢,是在(假定)已知道答案的基础上,对题目进行验证,即化求解为判定,降维打击,极大程度降低题目难度
ps:一般二分答案的外皮是 只求结果,不问过程
/*一道题正着来时间复杂度不仅高还难写,但带着答案(假定)验证简单得多(但是小伙伴们要注意啊,现在是逆着来,思路也该换换了。是化求解为判定,可能有些小伙伴们就卡住了,作者当初也卡这里了,思路没有立马换过来😂)*/
通俗一点 就是选择题带选项进行验证,只不过现在验证的手段换成了二分查找。
现在来说说 二分查找(int||float)的模板:
(妈妈再也不用担心我为边界问题而困惑了)
先是int模板
&1 check(mid) 你可以抽象成 函数,也可以看作 判定条件
/*作者一般是当作if的判定条件使用的,小伙伴们按照个人喜好来写即可*/
&2 >>1 相当于 /2
&模板1:
当我们将区间 [l,r] 划分成[l,mid] 和 [mid + 1,r] 时,其更新操作是 r = mid 或者l = mid + 1,计算 mid 时不需要加 1
while(l<r)
{
mid=(l+r)>>1;
if(check(mid)) r=mid;
else l=mid+1;
}
你可以理解为这是一个往左找答案的(建议不是很懂的小伙伴可以停下来思考思考,还是不懂跳到例1)
&模板2:
当我们将区间[l,r] 划分成 [l,mid -1 ] 和[mid,r] 时,其更新操作是 r = mid - 1或者I = mid;,此时为了防止死循环,计算 mid 时需要加 1
(理解理解,理解为重,作者写的时候也是在重温的)
while(l<r)
{
mid=(l+r+1)>>1;
if(check(mid)) l=mid;
else r=mid-1;
}
同样的,可以理解为往右找答案
最后是float模板
这就简单的多了,直接上操作
&模板3:
while(r-l>1e-5)//精度问题
{
mid=(l+r)/2;
if(check(mid)) r=mid;
else l=mid;
}
/*模板里的if-else都是可以换的,小伙伴们想怎么来怎么来*/
其实一般情况下 l,r 选取是无所谓的,相对差距大即可(其他情况依题而定)
但是还是建议 lr 按 题目给定区间选取,否则可能有1-2点wa摸不着头脑
由于if(check(mid))过于抽象,现在先来一题助小伙伴们理解
二分查找例题:
例1:P1102 A-B 数对
思路非常清晰,找到左右出现位置即可
法一(模板1&&2):
#include <iostream>
#include <algorithm>
using namespace std;
int a[200005];
int main()//1102
{
long long count=0;//有一个测试点count很大
int i,n,k,l,r,mid,com;
cin>>n>>k;
for(i=0;i<n;i++) cin>>a[i];
sort(a,a+n);
for(i=0;i<n-1;i++)//n-1 防止越界
{
r=n-1;//防止越界
l=0;//每次进来都要变为0
while(l<r)//左
{
mid=(l+r)>>1;
if(a[mid]>=a[i]+k) r=mid;//大于等于 如果不等于找不到最左
else l=mid+1;
}//出来后l==mid==r
if(a[l]!=a[i]+k) continue;//直接跳过
com=l;
l--;
r=n-1;
while(l<r)//右
{
mid=(l+r+1)>>1;
if(a[mid]<=a[i]+k) l=mid;//同样的等于 否则r会退
else r=mid-1;
}
count+=r-com+1;
}
cout<<count;
return 0;
}
法二(map):
#include<map> 是map的头文件
使用为:
键 映射
数据类型 数据类型
例: map<int ,string> 键-映射初值都是空(可以理解为 空格 || 0 )
#include<iostream>
#include<map>
using namespace std;
int main()
{
long long count=0,a[200005];
int i,x,k;
map<int,int> b;
cin>>x>>k;
for(i=0;i<x;i++)
{
cin>>a[i];
b[a[i]]++;//a[i]键 b[a[i]]映射
}
for(i=0;i<x;i++) count+=b[a[i]+k];
cout<<count;
return 0;
}
法三( lower_bound && upper_bound ):
lower_bound && upper_bound 的头文件是 algorithm
lower_bound 是对排序后数组第一个可插入且不改变单调性,且返回的是地址
upper_bound 即最后一个
/*用法自行体会(狗头)*/
#include<iostream>
#include<algorithm>
using namespace std;
long long a[200005],mycount=0,i,x,k;
int main()//1102
{
cin>>x>>k;
for(i=0;i<x;i++) cin>>a[i];
sort(a,a+x);
for(i=0;i<x;i++) mycount+=upper_bound(a,a+x,a[i]+k)-lower_bound(a,a+x,a[i]+k);
cout<<mycount;
return 0;
}//当count是全局变量时会报错
现在请小伙伴们重温一下模板,再次理解,即将开始 ‘攻城掠地’
例2:P2249 【深基13.例1】查找
思路:左
(纯纯模板题,小伙伴们肯定都会)
#include<iostream>
using namespace std;
int a[1000005];
int main()
{
int n,m,i,com,l,r,mid;
cin>>n>>m;
for(i=1;i<=n;i++) cin>>a[i];
for(i=0;i<m;i++)
{
cin>>com;
l=1;
r=n;
while(l<r)
{
mid=(l+r)/2;
if(a[mid]>=com) r=mid;
else l=mid+1;
}
if(a[l]==com) cout<<l<<" ";
else cout<<"-1 ";
}
return 0;
}
例3:P1678 烦恼的高考志愿
思路:贪心判断+ 左右二分 || algorithm库函数
#include <algorithm>
using namespace std;
int a[100005];
int main()
{
int m,n,i,k;
long long ans=0;
cin>>m>>n;
for(i=1;i<=m;i++) cin>>a[i];
sort(a+1,a+m+1);
a[0]=-5000000,a[m+1]=5000000;//这是唯一注意点,也是最大错因
for(i=1;i<=n;i++)
{
cin>>k;
if(a[lower_bound(a+1,a+m+1,k)-a]-k<k-a[lower_bound(a+1,a+m+1,k)-a-1]) ans+=a[lower_bound(a+1,a+m+1,k)-a]-k;
else ans+=k-a[lower_bound(a+1,a+m+1,k)-a-1];
}
cout<<ans;
return 0;
}
以上这些就是洛谷官方提单二分算法的二分查找例题了,对小伙伴们来说都是洒洒水的事👍
下面进入二分答案的环节
二分答案例题:
前面已经说过——二分答案就是选择题带选项进行验证,只不过现在验证的手段换成了二分查找。前面也已经说过,二分答案题具有以下几个特征:
&1 顺着麻烦,逆着简单(现在是验证,换换思路)
&2 只求结果,不问过程
&3 问题所求,最大最小
至于模板 1||2 ,最小值最大就是尽量往右,最大值最小就是尽量往左
废话不多说,上题
int二分答案
例4:P1873 [COCI 2011/2012 #5] EKO / 砍树
例4:P1873 [COCI 2011/2012 #5] EKO / 砍树
法一(数学):
这个主体时间复杂度是小于o(n)的,我看到这题第一反应就是正着来,因为很容易想到。不过二分答案不用想多少的(捂脸),“暴力”即可……
思路:排序后高树被砍掉部分大于等于所需即可,但是树高是整数,所以h是浮点数的话要 --
#include <iostream>
#include <algorithm>
using namespace std;
long long a[1000005],x,i,h;//数组太大放外面
int main()//1873
{
long long ans=0,m;
cin>>x>>m;
for(i=0;i<x;i++) cin>>a[i];
sort(a,a+x);
for(i=x-1;i>=0;i--)
{
ans+=a[i];
if(ans-a[i-1]*(x-i)>=m)
{
h=a[i]-(m-(ans-a[i]*(x-i)))/(x-i);
if((m-(ans-a[i]*(x-i)))%(x-i)!=0) h--;
cout<<h;
break;
}
}
return 0;
}
法二:
思路:右(最大右逼近)+验证(如果一个数是答案,那么如果 大于的树减去答案的部分 的和 就是所需木材)
验证主体内 不能等于mid,不懂的小伙伴们先思考,下面的例题一起讲清楚
因为是往右逼近,所以if内ans含=(一般情况下都是含等于的,还没遇到不等于的,大概也许是作者太蒟蒻了……)(欢迎小伙伴们和神犇们提高见)
#include <iostream>
#include <algorithm>
using namespace std;
long long a[1000005],x,i,h;
int main()//1873
{
long long ans,m,l=0,r,mid;
cin>>x>>m;
for(i=0;i<x;i++) cin>>a[i];
sort(a,a+x);
r=a[x-1];
while(l<r)
{
mid=(l+r+1)>>1;
ans=0;
for(i=0;i<x;i++)
{
if(a[i]>mid) ans+=a[i]-mid;
}
if(ans>=m) l=mid;
else r=mid-1;
}
cout<<l;
return 0;
}
例5:P2440 木材加工
思路:右(最大右逼近)+验证(如果一个数是答案,那么如果 每棵数除与答案的整数部分 的和 就是所需段数)
因为是往右逼近,所以if内ans含=
#include<iostream>
using namespace std;
int main()//2440
{
long long a[100005],ans,mid,k,l=0,r=100000000;
int n,i;
cin>>n>>k;
for(i=0;i<n;i++) cin>>a[i];
while(l<r)
{
ans=0;
mid=(l+r+1)>>1;
for(i=0;i<n;i++) ans+=a[i]/mid;
if(ans>=k) l=mid;
else r=mid-1;
}
cout<<r;
return 0;
}
例6:P2678 [NOIP2015 提高组] 跳石头
思路:右(最大右逼近)+验证
(回应上面的验证主体内等于问题:)
if(a[i]-a[com]<mid) count++;//不加等于,如果等于了,r就会变相往左了,此时不能保证右逼(也即假定有更大的答案)但是if内count还是要等于的
else com=i;(验证主体)
如果一个数是答案,并且两个石头的距离小于这个数——移,否则更新石头。然后count++,判定 count与给定移走数 的差距
#include<iostream>
using namespace std;
int main()
{
long long a[50005],l=0,r=1000000000,mid;
int len,n,m,i,count,com;
cin>>len>>n>>m;
for(i=1;i<=n;i++) cin>>a[i];
a[0]=0;
a[n+1]=len;
if(n==0)//不用移石头,直接输出
{
cout<<len;
return 0;
}
while(l<r)
{
mid=(l+r+1)>>1;
count=0;
com=0;
for(i=1;i<=n+1;i++)
{
if(a[i]-a[com]<mid) count++;
else com=i;
}
if(count<=m) l=mid;
else r=mid-1;
}
cout<<r;
return 0;
}
例7:P3853 [TJOI2007] 路标设置
这题……先前是绿的,现在是降级了(不过也确实,和例6差不多)
以及这题新增一个测试点
2 2 1
0 2
导致法二错这里
法二(与跳石头差不多,路标增):
思路:右(最大右逼近)+验证
while(com>mid)//是大于 不能含等于
{
count++;
com-=mid;
}(验证主体)
如果一个数是答案,那么如果 相邻两个路标差 大于这个数 就要设置路标,然后更新路标差。count++ 然后判定count与给定路标差距
验证部分可以写成除法,但是 和com不能等于mid 一样,当除法结果是整数时 -- (同例6)
#include<iostream>
using namespace std;
int a[100005];
int main()//3853
{
int len,n,k,now,before,i,l=0,r,com,mid,count;
cin>>len>>n>>k;
for(i=0;i<n;i++)
{
cin>>now;
if(i>0) a[i]=now-before;
before=now;
}
r=len;
while(l<r)
{
mid=(l+r)>>1;
if(mid==0)//mid==0卡死
{
cout<<"1";
return 0;
}
count=0;
for(i=1;i<n;i++)
{
com=a[i];
while(com>mid)//是大于 不能含等于
{
count++;
com-=mid;
}
}
if(count<=k) r=mid;
else l=mid+1;
}
cout<<l;
return 0;
}
法一(路标减):
思路:右(最大右逼近)+验证 (自行体会(狗头))
if(a[i]-com<=mid) com=a[i];//含等于
else
{
com+=mid;
i--;// --后++还是等于本身
count--;
}(验证主体)
至于为什么含等于,现在给定一组数据
17 4 1
0 12 14 17
但 mid==6 时,你会发现count只是减少了一次,为什么?因为 if内是更新当前路边位置啊(同例6),作者写出这种方法就是想提醒你 验证主体内 = 不能乱来
#include<iostream>
using namespace std;
int a[100005];
int main()//3853
{
int len,n,k,i,l=0,com,r,mid,count;
cin>>len>>n>>k;
for(i=0;i<n;i++) cin>>a[i];
r=len;
while(l<r)
{
mid=(l+r)>>1;
count=k;
com=0;
for(i=1;i<n;i++)
{
if(count<0) break;
if(a[i]-com<=mid) com=a[i];//含等于
else
{
com+=mid;
i--;// --后++还是等于本身
count--;
}
}
if(count>=0) r=mid;
else l=mid+1;
}
cout<<l;
return 0;
}
例8:P1182 数列分段 Section II
思路:左(最小左逼近)+验证
害怕了吧我的小伙伴,l=0第4个点wa
先看数据 作者这里也错了l=0(捂脸),还是看讨论板和题解知道的
5 4
1 2 3 4 5
易知答案为5,但是如果l=0,输出的是3(虽然也是被分成4段)显然不行,understand?
所以还是 建议 lr 按照题目选取 作者也会改正(狗头)
至于验证就是:如果一个数是答案,那么如果 这一段的和 加上下一段的一个数 的和 就会大于答案(理解成这样即可,贪心法理解),count++(分段),判定count
#include<iostream>
using namespace std;
int main()
{
long long r=0,sum,l=0,mid,a[100005];
int n,m,i,count;
cin>>n>>m;
for(i=0;i<n;i++)
{
cin>>a[i];
r+=a[i];
if(a[i]>l) l=a[i];
}
while(l<r)
{
mid=(l+r)>>1;
sum=0;
count=0;
for(i=0;i<n-1;i++)
{
sum+=a[i];
if(sum+a[i+1]>mid)
{
sum=0;
count++;
}
}
if(count<=m-1) r=mid;//加上最后一段
else l=mid+1;
}
cout<<l;
return 0;
}
float二分答案
终于没有麻烦的左右判定了🌹,也终于可以交给小伙伴自己了👍
例9:P1024 [NOIP2001 提高组] 一元三次方程求解
例9:P1024 [NOIP2001 提高组] 一元三次方程求解
思路:零点定理(麻烦一点点的浮点数二分答案模板题)
值得一提的点就是 if(fun(mid)*fun(r)>=0) r=mid; (写成l也是可以的)
#include<iostream>
using namespace std;
double a,b,c,d;
double fun(double x)
{
return a*x*x*x+b*x*x+c*x+d;
};
int main()
{
double l,r,mid,llim,rlim;
int i,res=0;
cin>>a>>b>>c>>d;
for(i=-100;i<101;i++)
{
l=i,r=i+1;
llim=fun(l);
rlim=fun(r);
if(llim==0.0)
{
printf("%0.2lf ",l);
res++;
}
if(llim*rlim<0.0)
{
while(r-l>1e-5)
{
mid=(l+r)/2;
if(fun(mid)*fun(r)>=0) r=mid;
else l=mid;
}
printf("%.2lf ",l);
res++;
}
if(res==3) return 0;
}
return 0;
}
例10:P1163 银行贷款
思路:(学理没有公式动不了一点)
公式:100/(1+0.029)^1+……+100/(1+0.029)^12=1000
#include<iostream>
#include<math.h>
using namespace std;
double x,m,n,l=0.0,r=3.0,mid;
double fun(double y)
{
double ans=0;
int i;
for(i=1;i<=n;i++) ans+=1/pow(1+y,i);
return x/m-ans;
}
int main()
{
cin>>x>>m>>n;
while(r-l>1e-5)
{
mid=(l+r)/2;
if(fun(mid)>=0) r=mid;
else l=mid;
}
printf("%0.1lf",l*100);
return 0;
}
例11:P3743 kotori的设备
思路:特判+贪心验证
注意:这题是浮点答案,不是以1秒为单位的。当然也是这个点,使得贪心验证法简单。
if(b[i]-a[i]*mid<0) sum+=(a[i]*mid-b[i]);//需要充电的 (验证主体)
至于特判很容易理解,所有设备用电量小于等于供电量即可。
如果一个数是答案,那么如果 每个不能在答案时间内 自给自足 的设备,都需要充电。然后判定 这些设备在这个答案时间内所需充电的总和 与 供电设备*答案时间 的差距即可
#include<iostream>
using namespace std;
int a[100005],b[100005];
int main()
{
int n,p,i;
double sum=0,mid,l,r=1e10;
cin>>n>>p;
for(i=0;i<n;i++)
{
cin>>a[i]>>b[i];
sum+=a[i];
}
if(sum<=p)
{
cout<<"-1";
return 0;
}
while(r-l>1e-6)
{
mid=(l+r)/2;
sum=0;
for(i=0;i<n;i++)
{
if(b[i]-a[i]*mid<0) sum+=(a[i]*mid-b[i]);//需要充电的
}
if(sum>=p*mid) r=mid;
else l=mid;
}
cout<<l;
return 0;
}
此至,小伙伴们11题全部AC🌹
最后
之前见过一个动图,情况是这样:里面有个写着算法的海,有个人在沙滩上往大海跑,跑着跑着被一块写着二分的石头绊倒了(这个动图作者应该是想表达对二分学习过程的痛苦吧,和作者曾经挺相似的(捂脸))
神犇和小伙伴们对此文章有任何高见都可以提,毕竟作者只是个蒟蒻(大哭)
二分呢,本人觉得吧,是基础算法里比较难搞的一个吧(还是那句话,算法一路——长路漫浩浩),当然我相信小伙伴们实力强大,功力深厚……
写完这篇题解,作者也是大彻大悟了,对二分算法看的多了一点。当然目光还是要往前的,二分也不能落,算法一路——长路漫浩浩啊。
期待下一次与你的相遇……