分塊——優化的暴力
對於很多的題目,我們都可以找到n^2的暴力算法。
但是,當n在10000到200000之間的時候,n^2基本穩穩卡掉。
發現,這樣的題目,經常還與區間有關系的時候,可以考慮分塊做法。
分塊,顧名思義,就是把待處理的整個大區間分成若幹塊。
口訣是:塊外暴力,塊內查表。
那麽這個塊的大小應該怎麽分呢??
應該是sqrt(n)大小。
證明:*&%&*^$#()&^%#$^&#$(^$%(並不會證)
自我感覺,分塊很巧妙的把各種復雜度都向sqrt(N)靠近
發現很多的題,都是時間復雜度n根號n,空間復雜度也是n根號n
而且不管是什麽操作,基本上都是根號n。既沒有n^2,也不存在O(1)
感覺,巧妙地把復雜度平均了一下。
分塊雖然是暴力,但是是一種非常有水平的暴力。
關鍵是狀態的設計,怎樣達到塊外暴力,塊內查表。
大概的感覺是,都要圍繞塊來進行設計。
而且,通常要有兩個以上函數狀態有機配合。。。
例題1:BZOJ 4241歷史研究
Description:
一句話題意:求區間加權眾數。
Solution:
f[i][j]表示從第i塊開頭到j的最大值 (查詢的時候,塊內查表)
cnt[i][j]表示從第i塊開始到序列末尾j出現了多少次(便於暴力中的後綴差分)
邊角余料處理一下就好啦~
Code:
一般情況下,對於塊的左右端點有兩種求法。
1.lower_bound(blo+1,blo+n+1,blo[x])塊的左端點。
lower_bound(blo+1,blo+n+1,blo[x]+1)塊的右端點+1
2.處理塊的時候,直接結構體記錄塊的左右端點。(推薦)
#include<bits/stdc++.h> using namespace std; typedef long long ll; const int N=100050; const int B=333; ll f[B][N],cnt[B][N]; int a[N],b[N],tot; int num[N],sta[N],top; int blo[N],BLO; int n,m; ll ans; int main() { scanf("%d%d",&n,&m); BLO=sqrt(n); for(int i=1;i<=n;i++)scanf("%d",&a[i]),blo[i]=(i-1)/BLO+1,b[i]=a[i]; sort(b+1,b+n+1);tot=unique(b+1,b+n+1)-b-1; for(int i=1;i<=n;i++)a[i]=lower_bound(b+1,b+n+1,a[i])-b; for(int i=1;i<=blo[n];i++){ ll now=0; for(int j=lower_bound(blo+1,blo+n+1,i)-blo;j<=n;j++){ cnt[i][a[j]]++,now=max(now,(ll)cnt[i][a[j]]*b[a[j]]),f[i][j]=now; } } int xx,yy; while(m--){ scanf("%d%d",&xx,&yy);ans=f[blo[xx]+1][yy]; int tmp=lower_bound(blo+1,blo+n+1,blo[yy])-blo;top=0; for(int i=tmp;i<=yy;i++) num[a[i]]++,sta[++top]=a[i]; tmp=lower_bound(blo+1,blo+n+1,blo[xx]+1)-blo; for(int i=xx;i<tmp;i++){ num[a[i]]++;sta[++top]=a[i]; ans=max(ans,(ll)((ll)num[a[i]]+cnt[blo[xx]+1][a[i]]-cnt[blo[yy]][a[i]])*b[a[i]]); } for(int i=1;i<=top;i++)num[sta[i]]=0; printf("%lld\n",ans); } return 0; }
例題2:[HNOI2010]彈飛綿羊
Description:
某天,Lostmonkey發明了一種超級彈力裝置,為了在他的綿羊朋友面前顯擺,他邀請小綿羊一起玩個遊戲。遊戲一開始,Lostmonkey在地上沿著一條直線擺上n個裝置,每個裝置設定初始彈力系數ki,當綿羊達到第i個裝置時,它會往後彈ki步,達到第i+ki個裝置,若不存在第i+ki個裝置,則綿羊被彈飛。綿羊想知道當它從第i個裝置起步時,被彈幾次後會被彈飛。為了使得遊戲更有趣,Lostmonkey可以修改某個彈力裝置的彈力系數,任何時候彈力系數均為正整數。
Solution:
(LCT裸題,我們不管這個,因為我不會)
發現,直接枚舉是n^2的。但是,許多的位置出發,可能會走到同一個位置,再跳出去。
類似於記憶化的思想。
假如設f[i]表示,從i開始,跳f[i]步出去。預處理倒序O(n)即可
查詢是O(1)的。
但是一旦修改了,就會整個前面的f都會變。就涼涼了。。
要是修改只改一部分就好了。
即使查詢不是O(1)也沒關系啊。
分塊,就來把這兩個復雜度平均一下,都變成sqrt(n)
維護f[i],g[i]
f[i] 表示跳幾次可以跳出所在塊
g[i] 表示跳出所在塊後到達的位置。
在查詢時,我們O(sqrt(n))的時間進行“整塊”的模擬,可以得到結果。
這樣,我們以塊為單位,出了塊就交給別人去管就可以了。
f,g的巧妙搭配,使得查詢只需要模擬即可!!
而且,修改時,只有塊內收到了影響。暴力倒序修改即可。
Code:
#include<bits/stdc++.h> using namespace std; const int N=200000+10; int n,m; int BLO,blo[N]; int f[N],g[N]; int a[N]; int ans; struct node{ int l,r; }kua[N]; int main() { scanf("%d",&n);BLO=sqrt(n); for(int i=1;i<=n;i++) { scanf("%d",&a[i]),blo[i]=(i-1)/BLO+1; if(!kua[blo[i]].l) kua[blo[i]].l=i; kua[blo[i]].r=i; } for(int i=1;i<=blo[n];i++){ for(int j=kua[i].r;j>=kua[i].l;j--){ int to=j; while(to<=kua[i].r){ if(!f[to]) to=to+a[j],f[j]++; else f[j]+=f[to],to=g[to]; } g[j]=to; } } scanf("%d",&m); int op,x,y; while(m--){ scanf("%d%d",&op,&x);x++; if(op==1){ ans=0; int to=x; while(to<=n){ ans+=f[to]; to=g[to]; } printf("%d\n",ans); } else{ scanf("%d",&y); a[x]=y; for(int i=kua[blo[x]].r;i>=kua[blo[x]].l;i--){ f[i]=g[i]=0; int to=i; while(to<=kua[blo[x]].r){ if(!f[to]) to=to+a[i],f[i]++; else f[i]+=f[to],to=g[to]; } g[i]=to; } } } return 0; }
分塊——優化的暴力