二分法的应用

二分法是一个非常高效的算法。它常常用于处理单调区间上的极值问题。

简而言之,当你要求出[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;
}

 

通过上述两道例题,我们发现二分法的关键是,将一个未知的变量变为已知,然后在用已知的变量来衡量未知的变量是否可行。其中突出的思想是模型的转化与利用,所以二分法是一个相当普遍和有趣的算法。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值