二分答案入門亂講
//聯賽之前寫博攢人品//
此博文,沒有格式。
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還寫了一個博。但他一直很不走心,我只是騙你們去加訪問量。