1.關於二分答案
//2017年12月有更新。
如果讀者沒有學過二分,那么我建議您把這個網站關掉。不是我有偏見或者什么,看這篇文章對不了解二分的人來說沒有好處。
對於一些問題,它的解滿足單調性,即如果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,ans=1; while(l<=r) { int mid=(l+r)>>1; if(check(mid))l=mid+1,ans=mid; else r=mid-1; } printf("%d",ans);
是啊,zz的板子誰都會。重點就在check函數上面。我們要使用時間復雜度優秀的check來寫。怎么寫出最好的方法呢?分析題目,優化算法,然后大膽猜想不用證明。多做題目,積累經驗(這是最吼的!)。然后因為我走歪了,check一向非主流,但莫名好用啊(有些題目的check只要有不合法就可以立即彈出,復雜度可以降到玄學級)一般二分答案復雜度是穩定的log(r-l+1),總復雜度是O(log*f(check))。
2.例題
下面分享一些題目,都是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,證明這個點在原點右邊,要往左邊挪。否則往右邊挪。
那就這么草率的結尾吧!