二分答案入门乱讲

//联赛之前写博攒人品//

此博文,没有格式。

1.关于二分答案

如果reader没有学过二分,那么我建议您把这个网站关掉。不是我有偏见或者什么,看这篇文章对不了解二分的人来说没有好处。

对于一些问题,它的解满足单调性,即如果x满足条件,则对于任意的 i ( 1<=i<=x) 或 (x <=i <=n) (假设1和n是答案的上下界)都会满足条件。一般遇上这种问题,我们就可以用二分答案来加快解决。这种问题常常有关键语句:使最大......最小。

对于上面的问题,在没学二分答案的时候,我们是这么写的:(假设答案是上界)

for(int i=1;i<=n;++i)
  if(!check(i))
  {
    Ans=i-1;
    break;
  }

枚举答案,进行检查。出现第一个不合法的解,答案就是它前面那个值。但这样看来,未免太慢。我们花了很长时间来检查每一个答案。如果答案是10000,我们就会做很多无用功。或者说我们枚举1000是正确的,那么前999个都可以看成白枚举了,很浪费。

那么我们该如何尽可能的少做这些无用功呢?

我们来设想一下。同样假设答案是上界,如果我们check了10000,发现它是满足解的,那么答案肯定不小于10000。如果我们又check了20000,发现它是满足解的,那么10000~20000内的数我们都不用枚举。又或者20000是不满足解的,那么答案就在10000~20000的左闭右开区间内。这个时候我们如果”恰当地“check 15000,答案的范围会进一步缩小。

看到这里我们大概都会想到二分了。一步一步地缩小答案范围最终出解。

身边的巴拉拉2016对我说:二分答案的板子大家都会啊。

	int L=1,R=n;
	while(L<=R)
	{
		int mid=(L+R)>>1;
		if(check(mid))L=mid+1;
		else R=mid-1;
	}
	printf("%d",L);

是啊,zz的板子谁都会。重点就在check函数上面。我们要使用时间复杂度最优的check来写。怎么写出最好的方法呢?分析题目,优化算法,然后大胆猜想不用证明。多做题目,积累经验。然后因为我走歪了,check一向非主流,但莫名好用啊。

下面分享一些题目,都是NOIP里面的水题,都是可以一遍AC的。

1.NOIP2015 河中跳房子

最大距离最小,一看就知道是二分答案。

那么该如何写check呢?

我们可以check当最大距离为mid时,所需要搬走的石头的个数。方法就是一个小小的贪心——能不拿走就不拿走,能少拿走就少拿走,然后一次check在O(n)的时间内跑过。总时间复杂度是O(nlogn)。

#include	
#include	
#include	
#include	
int L,M,N,pla[50100];
int gi()
{
	int x=0;char ch=getchar();
	while(ch>'9' || ch<'0')ch=getchar();
	while(ch>'/' && ch<':')x=(x<<1)+(x<<3)+ch-48,ch=getchar();
	return x;
}
int check(int x)
{
	int Ans=0,sta=0;
	for(int i=1;i<=N;++i)
	{
		while(pla[i]-sta>1,S=check(mid);
		if(S>M)r=mid-1;
		else l=mid+1;
	}
	printf("%d",r);
}
我们check的返回值就是最少需要搬走的石头个数,可以证明Ans就是最优值。


2.NOIP2012 借教室

因为答案具有单调性——答案之前的肯定都能借到。所以我们来二分答案。

那么该如何写check函数呢?

对于这道题,我们需要干的事有:区间增加和单点求值。可能大犇都想到了线段树,但这种复杂度贼高的算法最高只能过90分。我们没必要用高级算法,可以用一种小技巧处理——差分+前缀和。

所谓差分,就是把区间的操作转移到对区间端点的操作。比如我们对一个都是0的数组,要在5~13这段区间加上3,只要在5这个点上加上3,在14这个点上减掉3,这样在算前缀和的时候,0~4都是0,5~13都是3,14及以后又都成了0。这种操作修改是O(1),查询是O(n),但你可以一遍查询求出所有的点是否合法。所以查询的总复杂度就是O(n+m),题目的复杂度就是O((n+m)logm)。

#include	
#include	
#include	
using namespace std;
int n,m,num[1010000],d[1010000],sta[1010000],end[1010000],Q[1010000],L,R;
int gi()
{
	int x=0;char ch=getchar();
	while(ch>'9' || ch<'0')ch=getchar();
	while(ch>'/' && ch<':')x=(x<<1)+(x<<3)+ch-48,ch=getchar();
	return x;
}
bool check(int x)
{
	int total=0;
	for(int i=1;i<=n;++i)Q[i]=0;
	for(int i=1;i<=x;++i)Q[sta[i]]+=d[i],Q[end[i]+1]-=d[i];
	for(int i=1;i<=n;++i)
	{
		total+=Q[i];
		if(total>num[i])return false;
	}
	return true;
}
int main()
{
	n=gi();m=gi();
	for(int i=1;i<=n;++i)num[i]=gi();
	for(int i=1;i<=m;++i)d[i]=gi(),sta[i]=gi(),end[i]=gi();
	L=1;R=m;
	while(L<=R)
	{
		int mid=(L+R)>>1;
		if(check(mid))L=mid+1;
		else R=mid-1;
	}
	if(L>m)printf("0");
	else printf("-1\n%d",L);
	return 0;
}
我们check函数返回的是当前订单能否满足。


3.NOIP2011 聪明的质检员

一看就知道是二分答案:答案具有单调性,图像趋势类似于一个二次函数曲线,我们只要求出这个函数的顶点最近的整数就好了。二分答案,记录当前答案,比较一下就好了。

这里要解释一下那个式子的意思:L到R内所有满足Wj>W的j的个数乘以它们的体积和。这个可以用前缀和维护。开两个前缀和维护一下个数和体积和就好了。

#include	
#include	
#include	
#include	
#define LL long long int
LL n,m,s,l[201000],r[201000],V[201000],W[201000],Qnum[200100],QY[200100],maxR;
LL gi()
{
	LL x=0;char ch=getchar();
	while(ch>'9' || ch<'0')ch=getchar();
	while(ch>'/' && ch<':')x=x*10+ch-48,ch=getchar();
	return x;
}
LL max(LL x,LL y){return x>y?x:y;}
LL min(LL x,LL y){return x=x);
		QY[i]=QY[i-1]+V[i]*(W[i]>=x);
	}
	for(int i=1;i<=m;++i){Ans+=(Qnum[r[i]]-Qnum[l[i]-1])*(QY[r[i]]-QY[l[i]-1]);}
	return Ans-s;
}
int main()
{
	n=gi();m=gi();s=gi();
	for(int i=1;i<=n;++i)W[i]=gi(),V[i]=gi(),maxR=max(maxR,W[i]);
	for(int i=1;i<=m;++i)l[i]=gi(),r[i]=gi();
	{
		LL L=1,R=maxR,K=10000000000000;
		while(L<=R)
		{
			LL mid=(L+R)>>1,S=check(mid);
			if(S<0)R=mid-1,K=min(-S,K);
			else L=mid+1,K=min(S,K);
		}
		printf("%lld\n",K);
	}
	return 0;
}
check函数返回的是当前答案下的W的原值(不能带abs())

如果小于0,证明这个点在原点右边,要往左边挪。否则往右边挪。

不想写了,发了算了,看同学跳舞去了。

嘻嘻嘻嘻。

附:如果借教室没看懂,我旁边的巴拉拉2016也写了一个博。但以他高产低质的特性,我不指望你们能看懂。

如果质检员没看懂,我旁边的巴拉拉2016还写了一个博。但他一直很不走心,我只是骗你们去加访问量。

你可能感兴趣的:(算法与数据结构)