根號分治:
引入:
有這樣一類問題:有 \(n\) 個序列,\(m\) 個詢問,存在兩種做法:\(O(n^2)\) 預處理和 \(O(mn)\) 的不預處理.
顯然,兩種方法的復雜度都無法接受,因此考慮一種方法是否能平衡這種復雜度。
然后,就擁有了 根號分治 這種方法,思路和 分塊的整塊處理塊和枚舉處理 類似
一般來說,根號分治的題目可以分為 預處理階段 和 枚舉階段
分析:
根據一道題目引入:P3396 哈希沖突
題意:
給定 \(n\) 長序列,\(m\) 個操作:
A x y
詢問在序列下標模 \(x\) 時,余數為 \(y\) 的下標的對應的值的加和
C x y
把序列第 \(x\) 個數的值替換成 \(y\)
解決:
我們有兩種想法:
-
\(O(n^2)\) 處理 模 \(i\) 意義下值為 \(j\) 的答案,每次修改為 \(O(n)\).
-
每次詢問暴力求 \(O(mn)\) ,修改為 \(O(1)\)
這兩種肯定都不行。考慮優化:
對於模數小於 \(\sqrt{n}\) 的情況,按照第一種方法做,預處理為 \(O(n\sqrt{n})\), 查詢為 \(O(1)\)
對於模數大於 \(\sqrt{n}\) 的情況,模 \(\sqrt{n}\) 的結果為 \(i\) 的情況最多只有 \(\sqrt{n}\) 個數產生貢獻。因此用第二種方法:查詢修改復雜度為 \(O(\sqrt{n})\)
代碼:
#include<bits/stdc++.h>
using namespace std;
const int N=1.5e5+5,M=405;
char ch[5];
int n,m,x,y;
int dp[M][M],a[N],mod;
int main(){
cin>>n>>m; mod=sqrt(n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) for(int j=1;j<=mod;j++) dp[j][i%j]+=a[i];
while(m--){
scanf("%s%d%d",ch,&x,&y);
if(ch[0]=='A'){
if(x<=mod) printf("%d\n",dp[x][y]);
else{
int res=0; for(int i=y;i<=n;i+=x) res+=a[i]; printf("%d\n",res);
}
}
else{ for(int i=1;i<=mod;i++) dp[i][x%i]+=(y-a[x]); a[x]=y;}
}
return 0;
}
例題:
CF797E Array Queries
題意:
給定一個長度為 \(n\) 的序列 \(a\) 有 \(m\) 次詢問:
p k
要求不斷操作 \(p=p+a_p+k\) 直到 \(p>n\) ,求操作次數。
分析:
這道題和之前的題分析過程差不多,但是要預處理。
設 \(dp[i][j]\) 表示 \(i=p,j=k\) 的情況,此時的 根號情況設置成 \(k\). 因為如果刨除 \(a[i]\) 的值的影響, 整個遞推過程的時間復雜度只跟 \(k\) 的大小有關.
下標從大到小 遞推預處理: 通過 \(dp[i+a_i+j][j]\) 的情況推理出 \(dp[i][j]\) 的值,同時記得預處理 \(i+a_i+j>n\) 的情況,則有轉移方程:
if(i+a[i]+j>n) dp[i][j]=1;
else dp[i][j]=dp[i+a[i]+j][j]+1;
-
\(k\leq \sqrt{n}\) 直接輸出 \(dp[p][k]\)
-
\(k>\sqrt{n}\),遞推查詢 \(p=a[p]+k+p>n\) 的需要加的次數即可.
代碼:
#include<bits/stdc++.h>
using namespace std;
const int N=405,M=1e5+5;
int n,m;
int dp[M+1000][N],a[M];
int p,k,num;
int main(){
cin>>n; num=sqrt(n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=n;i>=1;i--)
for(int j=1;j<=num;j++){
if(i+a[i]+j>n) dp[i][j]=1;
else dp[i][j]=dp[i+a[i]+j][j]+1;
}
cin>>m;
while(m--){
scanf("%d%d",&p,&k);
if(k<=num) printf("%d\n",dp[p][k]);
else{
int res=0;
while(p<=n) res++,p=a[p]+k+p;
printf("%d\n",res);
}
}
return 0;
}
CF1039D You Are Given a Tree
題意:
給定一棵樹,求樹上擁有 \([1,n]\) 個節點的鏈的個數(一個點只能處於一條鏈上).
分析:
看數據范圍,我們又可以用這個根號分治的方法了!
這里求鏈上的節點個數可以使用 貪心 的方法:
首先預處理出來整個圖的 逆 \(dfs\) 序和每個點的父親. 通過兒子更新父親, 無遞歸過程而且不重復枚舉(優美的卡常).
如何判斷這個節點所在的鏈是否包含 \(num\) 個節點的鏈:
這個節點兩個不同兒子對應子鏈包含點數加和 \(\geq num\)
-
可行,那么就把答案增加 \(1\) ,並且標記此點不能再用.
-
否則,更新這個點 兒子對應子鏈節點數最大值
進行兩部分的時間復雜度分析:
設預處理次數為 \(T\) , 而且答案對於節點數的增加是單調遞減的,因此 枚舉階段 可以二分答案計算.
預處理: \(O(nT)\) , 枚舉: \(P(\frac\large{n^2 \log n}{T})\) (二分和搜索)
然后根據數學知識,這倆值相同時,加和最小. 所以 \(T=\sqrt{n\log n}\)
二分時注意事項:
我們判斷的是: \([i,lim]\) 區間的答案,所以下一個區間 \(i=lim+1\) 然后接下來的左端限制為 \(i\).... 可能語言讀起來比較奇怪,還是看代碼吧,這一點寫的時候改了好幾回.
代碼:
#include<bits/stdc++.h>
#define pii pair<int,int>
#define mk make_pair
using namespace std;
const int N=1e5+5;
int n,m,cnt,res,lim;
vector<int> g[N];
int f[N],dfn[N],ans[N],fa[N];
void dfs(int x,int last){
for(auto y:g[x]){
if(y==last) continue;
fa[y]=x; dfs(y,x);
}
dfn[++cnt]=x;
}
int solve(int num){//判斷是否包含 num 個節點的鏈: 兩個不同兒子對應子鏈包含點數加和>=num
int res=0; for(int i=1;i<=n;i++) f[i]=1;
for(int i=1;i<=n;i++){//根據dfs序,不可能情況重復
int y=dfn[i],x=fa[y];
if(!x||f[y]==-1||f[x]==-1) continue;
if(f[x]+f[y]>=num) res++,f[x]=-1;//標記此點不能再用
else f[x]=max(f[x],f[y]+1);
}
return res;
}
int main(){
cin>>n; m=sqrt(n*log2(n));//小優化
for(int i=1,x,y;i<n;i++){
scanf("%d%d",&x,&y); g[x].push_back(y); g[y].push_back(x);
}
dfs(1,0); ans[1]=n;
for(int i=2;i<=m;i++) ans[i]=solve(i);
for(int i=m+1;i<=n;i=lim+1){//答案是單調的
int now=solve(i),l=i,r=n;
while(l<=r){
int mid=l+r>>1;
if(solve(mid)==now) l=mid+1,lim=mid;
else r=mid-1;
}
for(int j=i;j<=lim;j++) ans[j]=now;
}
for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
// system("pause");
return 0;
}