倍增二分及与其它算法的结合
倍增二分
前言
现在最流行的二分算法是利用分治的思想,今天给大家介绍一种倍增思想的二分方法:倍增二分。其实已经有一些人想出了倍增二分并在使用,这里只是给大家做一总结。为了避免混淆,我们称之前的二分方式为朴素二分,新的二分方式为倍增二分。
思想
顾名思义,倍增二分其实和分治没有太大关系,主要是倍增的思想。因为任何一个十进制整数都能表示为二进制的形式,换言之,通过枚举二进制位和限制条件组成十进制整数,推出最后的答案。
过程
枚举每一个二进制位
如图,一般的单调关系可以表示为图①和图②的升序和降序。
举例
当然文字描述是十分抽象的,接下来举一个倍增二分查找的例子来说明。二分查找大家都知道,就是在一个单调的序列里查找一个数的位置。比如说我们要在数轴上找一个位置
时间复杂度及算法正确性证明
因为两种算法中的模拟部分,也就是 check 函数是一样的,所以我们只讨论在二分过程中两种算法不同的时间复杂度。
朴素二分:设
n 为序列长度。每次把区间从中间分成两半去找符合条件的那一个区间,相当于是把整个区间分成了一棵完全二叉树,深度最多有\lceil\log_2 n \rceil 层,及最多能分\lceil\log_2 n \rceil 次,时间复杂度为O(\log n) 。倍增二分:设
n 为序列长度。我们要保证初始能完全覆盖整个区间,即i=2^x \geq n ,初始值x=\lceil \log_2 n \rceil ,那么ans= \sum_{x=\lceil \log_2 n \rceil}^0q\times 2^x ,其中q \in \{0,1\} ,ans 可以覆盖到区间[1,n] 中的每一个数,即可以试探到区间[1,n] 的每一个位置,时间复杂度为O(\log n) 。(参考资料)
好处
既然倍增二分和朴素二分的复杂度是一样的,那么我们为什么要去使用它呢?我想原因大概有以下两点:
- 代码量较少于朴素二分,只需要一个
for循环枚举i ,再加一个if判断是否满足条件即可。 - 朴素二分解决有些问题时边界问题较为复杂,倍增二分则不用考虑朴素二分的边界问题。
倍增二分解决其它二分问题
实数域上的二分
实质上实数域上的二分和整数域上的二分十分相似,区别是实数是具有精度的,显然只枚举整数部分是达不到对精度的要求的,所以只需要让试探的长度
三分
我们发现函数值的增长率和 x 值存在着单调关系:如果函数波峰向上,函数值增长率会随着 x 增长而减小。那么便可以二分 x 值,控制它的增长量,因为是实数域,应按实数域上的二分处理。
倍增三分的鲁棒性
在实数域上的倍增三分可能会因为精度,比如说控制精度过大或过小而出现一些问题。虽然可能在练习时没有考虑那么多,代码有一些漏洞,但测试数据有限,常常也会 AC。可是为了面对比赛中的强大数据,建议养成好习惯:使用倍增三分法时,在推出最终的答案后和答案周围几个点再比对一下,可能会有更优的情况。
应用
这是一些涉及到二分题目的注意事项:
- 如果此题没有单调性,可以在允许的情况下先排序再二分。
- 要善用
lower_bound和upper_bound,如果只有二分查找可以运用,节省代码量。 - 模拟中要善用其他算法,和前缀和、差分等算法结合,更能体现二分的便利。
- 如果查找的是数组的下标,一定要判断不能越界,当心 RE。
例题
二分查找模板
P2249 【深基13.例1】查找
#include<bits/stdc++.h>
using namespace std;
int n,m,arr[1000005];
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
scanf("%d",&arr[i]);
while(m--)
{
int x,ans=0;
cin>>x;
for(int i=1<<30;i;i/=2)
if(ans+i<=n&&arr[ans+i]<x)
ans+=i;
if(arr[++ans]!=x)
cout<<"-1 ";
else
cout<<ans<<" ";
}
return 0;
}
二分答案模板
倍增二分答案实现时尽量要画出两个变量的单调关系,这样会比较直观,有助于分析解题。下面借此模板题给大家讲述一般倍增二分答案的做法。
P1824 进击的奶牛
如图我们发现牛的数量越少,相邻两头牛的最近距离就越大。而牛的数量题目已经给出,是
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int arr[N],n,c;
bool check(int x)
{
int cnt=1,sum=arr[1];
for(int i=2;i<=n;i++)
if(arr[i]-sum>=x)
{
cnt++;
sum=arr[i];
}
return cnt>=c;
}
int main()
{
cin>>n>>c;
for(int i=1;i<=n;i++)
scanf("%d",&arr[i]);
sort(arr+1,arr+n+1);
int ans=0;
for(int i=1<<30;i;i/=2)
if(check(i+ans))
ans+=i;
cout<<ans;
return 0;
}
三分模板
P3382 【模板】三分法
#include<bits/stdc++.h>
using namespace std;
const double eps=1e-10;
int n;
double l,r,a[15],ans;
double f(double x)
{
double sum=0;
for(int i=n;i>=0;i--)
sum+=a[i]*pow(x,i);
return sum;
}
int main()
{
cin>>n>>l>>r;
for(int i=n;i>=0;i--)
cin>>a[i];
double ans=l;
for(double i=r+1;i>=eps;i/=2)
{
double m1=ans+i,m2=ans+i+eps;
if(m1<=r&&f(m1)<f(m2))ans+=i;
}
printf("%0.5lf",ans);
return 0;
}
三分的鲁棒性
[ABC279D] Freefall
#include<bits/stdc++.h>
using namespace std;
#define int long long
const long double eps=1e-7;
int a,b,ans=-1;
long double check(long double g)
{
if(!g)return 1e18;
return b*1.l*(g-1)+a*1.l/sqrt(g);
}
signed main()
{
cin>>a>>b;
for(long long i=2e18;i;i/=2)
if(check(ans+i+1)>check(ans+i+2)||fabs(check(ans+i+1)-check(ans+i+2))<eps)
ans+=i;
long double pr=check(ans+2);
if(ans+1>=1)pr=min(pr,check(ans+1));
pr=min(pr,check(ans+3));
printf("%0.6Lf",pr);
return 0;
}
倍增二分与其它算法的结合
倍增二分与差分的结合
P1083 [NOIP2012 提高组] 借教室
简洁题意
有一段长度为
思路
看到题目发现租借教室的人数和教室数量存在单调关系,显然租借的人数越多,教室数量越少。那么便二分租借的人数,将不够租借的情况加入答案,最后答案加一。
处理细节
模拟中有
AC Code
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e6+5;
int n,m,r[N],d[N],s[N],t[N],ans,c[N],l;
bool check(int day)
{
l=0;
memset(c,0,sizeof(c));
for(int i=1;i<=day;i++)
{
c[s[i]]+=d[i];
c[t[i]+1]-=d[i];
}
for(int i=1;i<=n;i++)
{
l+=c[i];
if(l>r[i])return false;
}
return true;
}
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>r[i];
for(int i=1;i<=m;i++)
cin>>d[i]>>s[i]>>t[i];
if(check(m))
{
cout<<"0";
return 0;
}
for(int i=(1<<30);i;i/=2)
if(ans+i<=1e6&&check(ans+i))ans+=i;
cout<<"-1"<<endl<<ans+1;
return 0;
}
倍增二分与前缀和的结合
P1314 [NOIP2011 提高组] 聪明的质监员
简洁题意
给定
思路
看到题目发现参数
处理细节
和刚那题很像,也是要在每次模拟时遍历每一个区间,记录答案,时间复杂度为
AC Code
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e5+5;
int n,m,s,y,ans1,ans2;
int w[N],v[N],l[N],r[N],sum1[N],sum2[N];
int check(int W)
{
memset(sum1,0,sizeof(sum1));
memset(sum2,0,sizeof(sum2));
y=0;
for(int i=1;i<=n;i++)
{
if(w[i]>=W)
{
sum1[i]+=v[i];
sum2[i]++;
}
sum1[i]+=sum1[i-1];
sum2[i]+=sum2[i-1];
}
for(int i=1;i<=m;i++)
y+=(sum1[r[i]]-sum1[l[i]-1])*(sum2[r[i]]-sum2[l[i]-1]);
return y;
}
signed main()
{
cin>>n>>m>>s;
for(int i=1;i<=n;i++)
cin>>w[i]>>v[i];
for(int i=1;i<=m;i++)
cin>>l[i]>>r[i];
for(int i=(1ll<<50);i;i/=2)
{
if(check(ans1+i)>=s)ans1+=i;
if(check(ans2+i)>s)ans2+=i;
}
ans1=abs(check(ans1+1)-s);
ans2=abs(check(ans2)-s);
cout<<min(ans1,ans2);
return 0;
}
倍增二分与搜索的结合
P2658 汽车拉力比赛
简洁题意
给定一个
思路
看到题目发现能到达路标的个数和赛道的难度系数 D 存在单调关系,显然能到达路标的个数越多,难度系数 D 越大。于是可以二分难度系数 D,在 check 函数里跑 BFS,将无法到达所有路标的加进答案,最后答案加一。
AC Code
#include<bits/stdc++.h>
using namespace std;
const int N=505;
int n,m,a[N][N],ans=-1,cnt,x,y,sum;
bool v[N][N],t[N][N];
int dx[4]={-1,0,0,1},dy[4]={0,-1,1,0};
struct node
{
int x,y,now;
};
bool bfs(int d)
{
memset(v,0,sizeof(v));
sum=1;
queue<node>q;
q.push((node){x,y,1});
v[x][y]=1;
while(!q.empty())
{
node s=q.front();q.pop();
if(sum==cnt)return false;
for(int i=0;i<4;i++)
{
int xx=dx[i]+s.x,yy=dy[i]+s.y;
if(xx<1||xx>n||yy<1||yy>m)continue;
if(v[xx][yy]||abs(a[s.x][s.y]-a[xx][yy])>d)continue;
v[xx][yy]=1;
if(t[xx][yy])sum++;
q.push((node){xx,yy});
}
}
return true;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
cin>>t[i][j];
if(t[i][j])cnt++,x=i,y=j;
}
for(int i=(1<<30);i;i/=2)
if(bfs(ans+i))ans+=i;
cout<<ans+1;
return 0;
}
倍增二分与最短路的结合
P1462 通往奥格瑞玛的道路
简洁题意
给定一个有
思路
这道题和刚刚的那道题很像,其实就是把跑 BFS 换成跑 Dijkstra 或 SPFA,然后二分收取的费用,把每次跑完剩余的血量比
AC Code
Dijkstra:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e4+5;
int n,m,head[N],tot,b,x,y,dd,d[N],a[N],ans;
bool v[N],p=1;
struct node
{
int next,to,dis;
}edge[10*N];
void add(int from,int to,int dis)
{
tot++;
edge[tot].to=to,edge[tot].dis=dis;
edge[tot].next=head[from];
head[from]=tot;
}
int check(int cost)
{
if(cost<a[1]||cost<a[n])return 1e9;
memset(d,0x3f,sizeof(d));
memset(v,0,sizeof(v));
priority_queue<pair<int,int> >q;
q.push(make_pair(0,1));
d[1]=0;
while(!q.empty())
{
int x=q.top().second;
q.pop();
if(v[x])continue;
v[x]=1;
for(int i=head[x];i;i=edge[i].next)
{
int y=edge[i].to,dis=edge[i].dis;
if(a[y]<=cost&&v[y]==0&&d[y]>d[x]+dis)
{
d[y]=d[x]+dis;
q.push(make_pair(-d[y],y));
}
}
}
return d[n];
}
signed main()
{
cin>>n>>m>>b;
for(int i=1;i<=n;i++)
cin>>a[i];
while(m--)
{
cin>>x>>y>>dd;
add(x,y,dd);
add(y,x,dd);
}
for(int i=(1<<30);i;i/=2)
if(check(ans+i)>b)ans+=i;
else p=0;
if(p)cout<<"AFK";
else cout<<ans+1;
return 0;
}
SPFA:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=1e4+5;
int n,m,head[N],tot,b,x,y,dd,d[N],a[N],ans;
bool v[N],p=1;
struct node
{
int next,to,dis;
}edge[10*N];
void add(int from,int to,int dis)
{
tot++;
edge[tot].to=to,edge[tot].dis=dis;
edge[tot].next=head[from];
head[from]=tot;
}
int check(int cost)
{
if(cost<a[1]||cost<a[n])return 1e9;
memset(d,0x3f,sizeof(d));
memset(v,0,sizeof(v));
queue<int>q;
q.push(1);
d[1]=0;
v[1]=1;
while(!q.empty())
{
int x=q.front();
q.pop();
v[x]=0;
for(int i=head[x];i;i=edge[i].next)
{
int y=edge[i].to,dis=edge[i].dis;
if(a[y]<=cost&&d[y]>d[x]+dis)
{
d[y]=d[x]+dis;
if(!v[y])q.push(y);
}
}
}
return d[n];
}
signed main()
{
cin>>n>>m>>b;
for(int i=1;i<=n;i++)
cin>>a[i];
while(m--)
{
cin>>x>>y>>dd;
add(x,y,dd);
add(y,x,dd);
}
for(int i=(1<<30);i;i/=2)
if(check(ans+i)>b)ans+=i;
else p=0;
if(p)cout<<"AFK";
else cout<<ans+1;
return 0;
}