二分本质上是借助你要二分的某X具有的与其单调的某Y性质,来找到是否满足Y的边界。若要暴力,其实也能找到,二分不过是降低了时间复杂度而已。所以做题时,得先会暴力怎么写,过不了再继续二分优化。
//************************************二分答案*****************************************************//
1、洛谷P1873 [COCI 2011/2012 #5] EKO / 砍树
锯片高度与木材长度满足单调递减(单调不增)关系,锯片越高,得到的木材越短,又要找锯片最高高度,所以可以二分答案即锯片高度,找出满足木材长度与不满足之间的分界线。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1000010;
int n,m,q[N];
bool check(int x)
{
int sum=0;
for(int i=0;i<n;i++){
sum+=max(0,q[i]-x); //舍去负数结果
if(sum>=m) return true; //一旦满足就返回结果,提升速度
}
return false;
}
int main()
{
int hest=0;
cin>>n>>m;
for(int i=0;i<n;i++){
cin>>q[i];
hest=max(hest,q[i]);
}
int l=0,r=hest+1;
while(l+1<r){
int mid=(l+r)/2;
if(check(mid))
l=mid;
else
r=mid;
}
printf("%d",l);
return 0;
}
2、洛谷P2440 木材加工
和上一题几乎一模一样。。。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=100010;
int n,k,q[N];
bool check(int x)
{
int sum=0;
for(int i=0;i<n;i++){
sum+=q[i]/x;
if(sum>=k) return true;
}
return false;
}
int main()
{
int lest=0;
cin>>n>>k;
for(int i=0;i<n;i++){
cin>>q[i];
lest=max(lest,q[i]);
}
int l=0,r=lest+1;
while(l+1<r){
int mid=(l+r)/2;
if(check(mid))
l=mid;
else
r=mid;
}
printf("%d",l);
return 0;
}
//************************************更复杂的模拟**************************************************//
只要你会暴力(模拟出过程)和二分模板,就能做出来!
3、P2678 [NOIP2015 提高组] 跳石头
最短跳跃距离与移动次数单调不减。
这个check怎么实现呢?check函数每个题有每个题的写法,但大体上的思想应该都是一样的——想办法检测这个解是不是合法。拿这个题来说,我们去判断如果以这个距离为最短跳跃距离需要移走多少块石头,先不必考虑限制移走多少块,等全部拿完再把拿走的数量和限制进行比对,如果超出限制,那么这就是一个非法解,反之就是一个合法解。
去模拟这个跳石头的过程。开始你在i(i=0)位置,我在跳下一步的时候去判断我这个当前跳跃的距离,如果这个跳跃距离比二分出来的mid小,**那这就是一个不合法的石头,应该移走。**为什么?我们二分的是最短跳跃距离,已经是最短了,如果跳跃距离比最短更短岂不是显然不合法,是这样的吧。移走之后要怎么做?先把计数器加上1,再考虑向前跳啊。去看移走之后的下一块石头,再次判断跳过去的距离,如果这次的跳跃距离比最短的长,那么这样跳是完全可以的,我们就跳过去,继续判断,如果跳过去的距离不合法就再拿走,这样不断进行这个操作,直到第n+1次,为啥是n+1?河中间有n块石头,显然终点在n+1处。
模拟完这个过程,我们查看计数器的值,这个值代表的含义是我们以mid作为答案需要移走的石头数量,然后判断这个数量 是不是超了就行。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=50010;
int L,n,m,q[N];
bool check(int x)
{
int i=0,now=0;
int cnt=0;
while(i<n+1){
i++;
if(q[i]-q[now]<x)
cnt++;
else
now=i;
}
if(cnt<=m)
return true;
else
return false;
}
int main()
{
cin>>L>>n>>m;
for(int i=1;i<=n;i++)
cin>>q[i];
q[n+1]=L;
int l=0,r=L+1;
while(l+1<r){
int mid=(l+r)/2;
if(check(mid))
l=mid;
else
r=mid;
}
cout<<l;
return 0;
}
4、P3853 [TJOI2007]路标设置
最大距离与路标数单调不增。
思路和上一题相似,注意输入的q[N]是与起点的距离不是路标间距;check函数改一改:比最大距离大就插路标。。。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=100010;
int L,n,k,q[N];
bool check(int x)
{
int dx[N];
int cnt=0;
for(int i=1;i<n;i++){
dx[i]=q[i]-q[i-1];
while(dx[i]>x){ //在你放一个路标后其间距仍可能大于x,故需循环处理
cnt++;
dx[i]-=x;
}
}
if(cnt<=k)
return true;
else
return false;
}
int main()
{
cin>>L>>n>>k;
for(int i=0;i<n;i++)
cin>>q[i];
int l=0,r=L+1;
while(l+1<r){
int mid=(l+r)/2;
if(check(mid))
r=mid;
else
l=mid;
}
cout<<r;
return 0;
}
5、P1182 数列分段 Section II
和4题思路相同,有些细节注意下。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=100010;
int n,m,q[N];
int l,r;
bool check(int x)
{
int now=0;
int cnt=1; //cnt-->段数,最初就是一段
for(int i=0;i<n;i++){
if(now+q[i]>x){ //当目前的和大于x
cnt++; //插线
now=0; //重“新”开始
}
now+=q[i]; //新-->q[i]
}
if(cnt<=m)
return true;
else
return false;
}
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++){
cin>>q[i];
l=max(l,q[i]);
r+=q[i];
}
l--;r++; //若l不减1,则第一个答案永远都不在满足check函数的那一边,哪怕它是满足的;r++同理
while(l+1<r){
int mid=(l+r)/2;
if(check(mid))
r=mid;
else
l=mid;
}
cout<<r;
return 0;
}
6、P3743 kotori的设备
由于充电的过程是连续的,并且切换设备的时间忽略不计,所以只要满足我们要充的电的总量比能充的电的总量小,就一定有一种充电方式,不需要担心中途没电的问题:我先给最快没电的那个设备充电,充到它和当前最快没电的设备没电的时间相同时停止充电,然后再给新的最快没电的设备充电。
充电时间和可充总能量单调递增。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=100010;
int n;
double p,a[N],b[N],sum;
bool check(double x)
{
double q=p*x; //可充的总能量
double all=0; //需消耗的总能量
for(int i=0;i<n;i++){
if(a[i]*x<=b[i]) //设备原有能量 能供给消耗
continue;
all+=(a[i]*x-b[i]); //不能 供给则要耗能
}
if(all<=q)
return true;
else
return false;
}
int main()
{
cin>>n>>p;
for(int i=0;i<n;i++){
cin>>a[i]>>b[i];
sum+=a[i];
}
if(sum<=p){ //需消耗总和小于充电速度
cout<<-1;
return 0;
}
double l=0,r=1e10; //过不了 r开大一点
while(r-l>1e-6){
double mid=(l+r)/2;
if(check(mid))
l=mid;
else
r=mid;
}
cout<<l;
return 0;
}
/*
我们的充电并不是一次性充了 a[i]*x-b[i],而是充了多次,他的充电时间和为这个值!为什么可以这样做呢?
首先换手机充电的操作是瞬间完成,其次电量变化是连续的。这也就给了我们充一会一把手机,让它能用到我把其他手机充电到能用到相同时刻的电量的可能。这样我们就可以一直按这样做下去,使其使用时间尽量长。因为使用时间可以二分找到,那么我们对于每把手机的充电量也就可以全部一次性算出来,而不用一份一份算啦。
所以我们在 check 函数里判断时间可不可行虽然是整段算的,但实际上它被分为很多细小的部分分别进行充电。
*/
//***********************************************浮点数二分**********************************************//
7、P1163 银行贷款
利率越大,需还款数越大,其单调递增。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
int a,b,c;
bool check(double x)
{
double d=a; //剩余还付
for(int i=0;i<c;i++) //模拟还款过程
d=d-b+d*x/100;
if(d<0.0001) //还完了
return true;
else
return false;
}
int main()
{
cin>>a>>b>>c;
double l=0,r=1000; //若过不了,r可以再开大一点
while(r-l>0.0001){ //确定精度
double mid=(l+r)/2;
if(check(mid))
l=mid;
else
r=mid;
}
printf("%.1f",l);
return 0;
}
8、P1024 [NOIP2001 提高组] 一元三次方程求解
因为区间很大,所以可以二分。
三个答案都在[-100,100]范围内,两个根的差的绝对值>=1,保证了每一个大小为1的区间里至多有1个解,也就是说当区间的两个端点的函数值异号时区间内一定有一个解,同号时一定没有解。/那么我们可以枚举互相不重叠的每一个长度为1的区间,在区间内进行二分查找。
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
double a,b,c,d;
double fc(double x)
{
return a*x*x*x+b*x*x+c*x+d;
}
int main()
{
double l,r,m,y1,y2;
int cnt=0;
scanf("%lf%lf%lf%lf",&a,&b,&c,&d);
for (int i=-100;i<100;i++)
{
l=i;
r=i+1;
y1=fc(l);
y2=fc(r);
if (cnt==3) break;
if(y1==0) //判断左端点,是零点直接输出。
{
printf("%.2lf ",l);
cnt++;
} //不能判断右端点,会重复。
if(y1*y2<0) //区间内有根。
{
while(r-l>=0.001)
{
m=(l+r)/2;
if(fc(m)*fc(r)<=0)
l=m;
else
r=m;
}
printf("%.2lf ",l);
cnt++;
}
}
return 0;
}
但这题可以暴力。。
#include <iostream>
using namespace std;
int main()
{
double a,b,c,d;
scanf("%lf%lf%lf%lf",&a,&b,&c,&d);
for(double i=-100;i<=100;i+=0.001)
{
double j=i+0.001;
double y1=a*i*i*i+b*i*i+c*i+d;
double y2=a*j*j*j+b*j*j+c*j+d;
if(y1>=0&&y2<=0||y1<=0&&y2>=0)
//printf("%.2lf ",i);
//printf("%.2lf ",j);
printf("%.2lf ",(i+j)/2);
}
return 0;
}
//*********************************利用二分边界求值****************************************//
9、P1102 A-B 数对
等式变形为A=B+C,C已知,枚举B即可得到A值,求出两次的边界间有多少值=A即可。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=200010;
int n,c,q[N];
long long cnt;
int main()
{
cin>>n>>c;
for(int i=0;i<n;i++)
cin>>q[i];
sort(q,q+n);
for(int b=0;b<n-1;b++){
int a=q[b]+c; //枚举B,由 C得 A,
int l=-1,r=n; //再二分查找A在数组中的位置,
while(l+1<r){
int mid=(l+r)/2;
if(q[mid]<=a)
l=mid;
else
r=mid;
}
if(q[l]==a) cnt+=l+1; //再计算A出现了多少次 :l1-r2+1
else continue;
l=-1,r=n;
while(l+1<r){
int mid=(l+r)/2;
if(q[mid]<a)
l=mid;
else
r=mid;
}
cnt-=r;//想一想为什么不用判断
}
cout<<cnt;
return 0;
}
10、P1678 烦恼的高考志愿
枚举学生分数,用二分将离学生分数最近的学校分数(边界)找出。
#include<iostream>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=100010;
int main()
{
int m,n;
long long sum=0;
int xx[N],xs[N];
scanf("%d%d",&m,&n);
for(int i=0;i<m;i++) scanf("%d",&xx[i]);
sort(xx,xx+m);
for(int i=0;i<n;i++) scanf("%d",&xs[i]);
for(int i=0;i<n;i++){
int l=0,r=m-1;
while(l+1<r){
int mid=(l+r)/2;
if(xx[mid]<xs[i]) l=mid;
else r=mid;
}
int minn=min(abs(xs[i]-xx[l]),abs(xx[r]-xs[i]));
sum+=minn;
}
printf("%lld",sum);
return 0;
}