二分法是一个非常高效的算法。它常常用于处理单调区间上的极值问题。
简而言之,当你要求出[a,b]区间使得F(x)>K的第一个x时,若 f(x)满足在区间[a,b]
内单调。则可以重复一下步骤直至找到解为止。
1. 选取当前区间中点S,并计算出f(s)
2. 若f(s)>x,则跳到第3步,否则跳到第4步。
3. 将当前区间转变为[s,b],跳到第5步
4. 将当前区间转变为[a,s],跳到第5步
5. 若当前区间已经满足求解要求则退出,否则跳到第1步。
因为每次二分都将原来的区间减少一半,所以时间复杂度是log*判定的时间。
二分法在题目中的应用价值就是,它将在一段单调区间内求极值的问题转化成了判定性问题。这使得很多不能直接做的问题变得简单。下面就举几个例子。
(1) 例题1:
按顺序给出N个数,要求分成M组连续的元素段,使得段内元素的权值之和最大的那个元素段的元素的权值和最小。保证权值均为正数。(1<=M<=N<=1000000)
初看之下这道题目是N^2*M的动态规划,但是明显时间复杂度超鬼了。
那么该如何处理呢?
二分!当我们面对是这样一个问题时,题目将变得简单许多
转化后的问题:按顺序给出N个数,要求分成最少组连续的元素段,使得每个元素段内的元素的权值之和均小于或等于K。
面对转化后的问题,我们发现可以贪心处理。尽可能地将每一组填满,如果遇到不能放进去的元素或者是最少的分组大于M则判断为不行,否则判断为行。按照上述方法写成程序,时间复杂度为O(logLimit*N),相较于动态规划的算法要好太多了。
#include <iostream>
#include <cstdio>
#include <stdint.h>
using namespace std;
#define ULL unsigned long long
int n,m;
ULL l,r;
int s[1000010];
int num[1000010];
int maxn[1000010];
ULL MAXN;
ULL ans=18446744073709551615ULL;
bool pan(int l){
int i=0,cnt=1;
ULL sum=0;
while(1){
i++;sum+=s[i];
if(sum>l){
if(s[i]>l)return false;
sum=s[i];
cnt++;
if(cnt>m)return false;
}
if(i==n)break;
}
return true;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&s[i]),MAXN+=s[i];
l=1;r=MAXN;
while(l<=r){
ULL mid=(l+r)/2;
bool flag=pan(mid);
if(flag){
if(ans>mid)ans=mid;
if(l==r)break;
r=mid;
}
else l=mid+1;
}
printf("%I64u\n",ans);
return 0;
}
(2) 例题2:
给定一带权无向图G,其中包含N个点和M条边,每条边有两个权值A和B,现在给定两个点S和T,求从S到T的一条路径,使得路径上权值A之和与权值B之和的比最小。求这个最小的比率。(N<=1000,M<=40000)
首先,貌似又只能用动态规划做,而且时间复杂度超爆。
再次将模型转化一下。
转化后的问题:
给定一个比率K,问在带权无向图G中是否存在一条从S到T的路径使得这条路径上的权值A之和与权值B之和的比率不大于K。
这个问题看似也很难做,但是我们可以做如下的转化。对于每一条边(u,v,a,b),我们将权值wi=ai-bi*k。于是,当从S到T存在着一条路径使得其新的权值和小于等于0时,就一定存在一条从S到T的路径使得其比率小于等于K。那么,明显取最短的路径来与0比较是最优的,于是题目就变成了最短路问题。
#include <iostream>
#include <cstdio>
#include <queue>
#include <cstring>
using namespace std;
struct Edge{
int l,r,a,b;
};
queue<int>q;
Edge ins[40010];
int head[1010],data[80010],nxt[80010];
double wei[80010];
int cn[1010];
double from[1010];
bool in[1010];
double dis[1010];
double k,l=0,r=10000,ans=100000;
int n,m,s,t,cnt;
void add(int x,int y,double weight){
nxt[cnt]=head[x];wei[cnt]=weight;data[cnt]=y;head[x]=cnt++;
nxt[cnt]=head[y];wei[cnt]=weight;data[cnt]=x;head[y]=cnt++;
}
bool spfa(){
memset(dis,65,sizeof dis);
memset(in,0,sizeof in);
memset(from,0,sizeof from);
memset(cn,0,sizeof cn);
dis[s]=0;in[s]=true;q.push(s);
while(!q.empty()){
int now=q.front();
q.pop();in[now]=false;
for(int i=head[now];i;i=nxt[i]){
if(dis[data[i]]>dis[now]+wei[i]){
from[data[i]]=wei[i]+from[now];
dis[data[i]]=dis[now]+wei[i];
cn[data[i]]++;
if(cn[data[i]]==n+1)return true;
if(!in[data[i]]){
in[data[i]]=true;
q.push(data[i]);
}
}
}
}
if(from[data[t]]<=0)return true;
return false;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)scanf("%d%d%d%d",&ins[i].l,&ins[i].r,&ins[i].a,&ins[i].b);
scanf("%d%d",&s,&t);
while(r-l>=1e-10){
k=(l+r)/2;
cnt=1;
memset(head,0,sizeof head);memset(nxt,0,sizeof nxt);
memset(wei,0,sizeof wei);memset(data,0,sizeof data);
for(int i=1;i<=m;i++)add(ins[i].l,ins[i].r,ins[i].a-ins[i].b*k);
bool flag=spfa();
if(flag){
if(ans>k)ans=k;
if(r==k)break;
r=k;
}
else{
if(l==k)break;
l=k;
}
}
printf("%lf\n",ans);
return 0;
}
通过上述两道例题,我们发现二分法的关键是,将一个未知的变量变为已知,然后在用已知的变量来衡量未知的变量是否可行。其中突出的思想是模型的转化与利用,所以二分法是一个相当普遍和有趣的算法。