1. 程式人生 > >二分答案入門亂講

二分答案入門亂講

//聯賽之前寫博攢人品//

此博文,沒有格式。

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的。

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

那麼該如何寫check呢?

我們可以check當最大距離為mid時,所需要搬走的石頭的個數。方法就是一個小小的貪心——能不拿走就不拿走,能少拿走就少拿走,然後一次check在O(n)的時間內跑過。總時間複雜度是O(nlogn)。

#include	<iostream>
#include	<cstdio>
#include	<cstdlib>
#include	<algorithm>
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<x && i<=N)
		{
			Ans++;i++;
		}
		sta=pla[i];
	}
	return Ans;
}
using namespace std;
int main()
{
	L=gi();N=gi();M=gi();pla[0]=0;
	for(int i=1;i<=N;++i)pla[i]=gi();
	pla[++N]=L;
	int l=1,r=L;
	while(l<=r)
	{
		int mid=(l+r)>>1,S=check(mid);
		if(S>M)r=mid-1;
		else l=mid+1;
	}
	printf("%d",r);
}
我們check的返回值就是最少需要搬走的石頭個數,可以證明Ans就是最優值。

因為答案具有單調性——答案之前的肯定都能借到。所以我們來二分答案。

那麼該如何寫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	<iostream>
#include	<cstdio>
#include	<cstdlib>
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函式返回的是當前訂單能否滿足。

一看就知道是二分答案:答案具有單調性,影象趨勢類似於一個二次函式曲線,我們只要求出這個函式的頂點最近的整數就好了。二分答案,記錄當前答案,比較一下就好了。

這裡要解釋一下那個式子的意思:L到R內所有滿足Wj>W的j的個數乘以它們的體積和。這個可以用字首和維護。開兩個字首和維護一下個數和體積和就好了。

#include	<cstdio>
#include	<cstdlib>
#include	<iostream>
#include	<algorithm>
#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<y?x:y;}
LL check(LL x)
{
	LL Ans=0;
	for(int i=1;i<=n;++i)
	{
		Qnum[i]=Qnum[i-1]+(W[i]>=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還寫了一個。但他一直很不走心,我只是騙你們去加訪問量。