根號分治


根號分治:

引入:

有這樣一類問題:有 \(n\) 個序列,\(m\) 個詢問,存在兩種做法:\(O(n^2)\) 預處理和 \(O(mn)\) 的不預處理.

顯然,兩種方法的復雜度都無法接受,因此考慮一種方法是否能平衡這種復雜度。

然后,就擁有了 根號分治 這種方法,思路和 分塊的整塊處理塊和枚舉處理 類似

一般來說,根號分治的題目可以分為 預處理階段枚舉階段

分析:

根據一道題目引入:P3396 哈希沖突

題意:

給定 \(n\) 長序列,\(m\) 個操作:

A x y 詢問在序列下標模 \(x\) 時,余數為 \(y\) 的下標的對應的值的加和
C x y 把序列第 \(x\) 個數的值替換成 \(y\)

解決:

我們有兩種想法:

  1. \(O(n^2)\) 處理 模 \(i\) 意義下值為 \(j\) 的答案,每次修改為 \(O(n)\).

  2. 每次詢問暴力求 \(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; 
  1. \(k\leq \sqrt{n}\) 直接輸出 \(dp[p][k]\)

  2. \(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. 可行,那么就把答案增加 \(1\) ,並且標記此點不能再用.

  2. 否則,更新這個點 兒子對應子鏈節點數最大值

進行兩部分的時間復雜度分析:

設預處理次數為 \(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;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM