與圖論的邂逅09:樹上啓發式合併

啓發式樹上合併

先看這樣一個例題:

給定一個長度爲\(10^5\)的序列,序列中都是\([0,1000000]\)的整數,接下來有\(10^5\)次詢問,共兩種詢問:修改序列某個位置的值,以及查詢區間\([L,R]\)內一共有多少種不同的數。

先不考慮線段樹的做法。我們可以想到用莫隊來解這道題———按莫隊的套路排序後用桶來維護即可,至多1000000個桶,複雜度\(O(N\sqrt{N})\)。可以看到,我們的核心思路就是排序後改變了每個位置的訪問順序,從而降低了複雜度。

然後我們看到下面的例題(如果上面不是爲了引出dsu我會亂說?):

給定一棵大小爲\(10^6\)的樹,樹上每個點都是一個\([0,1000000]\)的整數,接下來有\(10^5\)次詢問,查詢某棵子樹內一共有多少種不同的數。

樹上莫隊?不過我現在需要\(NlogN\)的算法。那麼我們參考一下莫隊的思想,可不可以通過修改訪問順序來降低複雜度?這個算法叫“樹上啓發式合併”,一般寫\(dsu\)

考慮暴力的做法,就是暴力處理出所有子樹的答案,再\(O(1)\)回答。遞歸遍歷整棵樹,然後對於每個點\(u\),暴力計算\(u\)子樹的答案。複雜度爲\(O(N^2)\)

在我們計算完\(u\)的兒子\(v\)的答案後,我們需要計算\(v\)的答案對\(u\)的貢獻,然後把\(v\)的答案刪去。於是我們發現,最後一棵子樹的答案是不需要刪除的。對比我們人的思維,我們肯定會把大的子樹放到最後處理。然後我們發現了另一個優美的東西:重鏈剖分。由於重鏈剖分的做法,我們每次往輕兒子走之後,樹的大小都會減少至少\(\frac{1}{2}\),所以每個節點到根的路徑上至多有\(\log_2n\)條輕邊,進而得出每個點被輕邊連接的祖先最多遍歷\(\log_2n\)次。那麼我們把重子樹放到最後處理,優先走輕邊,邊走邊暴力處理輕子樹的答案的話,那麼任意重兒子到根的路徑中的所有重邊連接的祖先在計算完它們的輕子樹的答案前是不會往下遍歷到這個點的,所以一個點被遍歷的次數就等於\(log_2n+1\)。複雜度就是\(NlogN\)

總結起來說,算法流程基本就是:\(O(n)\)重鏈剖分,\(O(nlogn)\)樹上啓發式合併,其中每遍歷到一個點時先處理輕子樹的答案,用輕子樹的信息更新自己的答案,然後刪掉輕子樹的信息,遍歷重兒子。最後\(O(m)\)回答。


這裏有一個例題:

CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths

一棵根爲1 的樹,每條邊上有一個字符(a-v共22種)。一條簡單路徑被稱爲Dokhtar-kosh當且僅當路徑上的字符經過重新排序後可以變成一個迴文串。求每個子樹中最長的Dokhtar-kosh路徑的長度。

\(1{\leq}n{\leq}5·10^5\)


很明顯的性質是:迴文路徑上出現次數爲奇數的字符至多隻有一個。那麼我們可以狀壓,設\(i\)表示路徑上字符出現次數的奇偶狀態,那麼至多隻有22種情況,枚舉即可。

由於要求最長路徑的長度,我們設\(u\)\(v\)的祖先,並且u到根的路徑的奇偶狀態與v相同,那麼\(v\)顯然優於\(u\),因爲\(v\)的深度更大。那麼我們設一個數組\(cnt_i\)表示路徑狀態爲\(i\)的最大深度。

如何得到一條路徑的狀態?設路徑的兩端點爲\(u,v\),那麼狀態就是\(state_u\)^\(state_v\)^\(state_{lca}\)^{state_{lca}},其中\(state_x\)表示\(x\)到根的路徑的奇偶狀態。

那麼我們的算法就是:先\(O(n)\)重鏈剖分,並且處理出\(state\)數組;然後\(dsu\)。時間複雜度爲\(O(nlogn)\)

#include<iostream>
#include<cstring>
#include<cstdio>
#define maxn 500010
using namespace std;

int dep[maxn],son[maxn],size[maxn],ldfn[maxn],rdfn[maxn],id[maxn],dfn;
int col[maxn],cnt[(1<<22)-1],state[maxn],ans[maxn];
int n,nowson;

struct edge{
    int to,col,next;
}e[maxn<<1];
int head[maxn],k;

inline void add(const int &u,const int &v,const int &w){
    e[k]=(edge){v,w,head[u]},head[u]=k++;
}

void dfs_getson(int u,int fa){
    size[u]=1,ldfn[u]=++dfn,id[dfn]=u;
    for(register int i=head[u];~i;i=e[i].next){
        int v=e[i].to;
        if(v==fa) continue;
        dep[v]=dep[u]+1,state[v]=state[u]^e[i].col,dfs_getson(v,u),size[u]+=size[v];
        if(size[v]>size[son[u]]) son[u]=v;
    }
    rdfn[u]=dfn;
}

void dsu(int u,int fa,bool op){
    for(register int i=head[u];~i;i=e[i].next){
        int v=e[i].to;
        if(v==fa||v==son[u]) continue;
        dsu(v,u,false),ans[u]=max(ans[u],ans[v]);
    }
    if(son[u]) dsu(son[u],u,true),ans[u]=max(ans[u],ans[son[u]]);
    if(cnt[state[u]]) ans[u]=max(ans[u],cnt[state[u]]-dep[u]);
    for(register int i=0;i<22;++i) if(cnt[state[u]^(1<<i)])
        ans[u]=max(ans[u],cnt[state[u]^(1<<i)]-dep[u]);
    cnt[state[u]]=max(cnt[state[u]],dep[u]);
    for(register int i=head[u];~i;i=e[i].next){
        register int v=e[i].to;
        if(v==fa||v==son[u]) continue;
        for(register int j=ldfn[v];j<=rdfn[v];++j){
            register int w=id[j];
            if(cnt[state[w]]) ans[u]=max(ans[u],cnt[state[w]]+dep[w]-2*dep[u]);
            for(register int k=0;k<22;++k) if(cnt[state[w]^(1<<k)])
                ans[u]=max(ans[u],cnt[state[w]^(1<<k)]+dep[w]-2*dep[u]);
        }
        for(register int j=ldfn[v];j<=rdfn[v];++j) cnt[state[id[j]]]=max(cnt[state[id[j]]],dep[id[j]]);
    }
    if(!op) for(register int i=ldfn[u];i<=rdfn[u];++i) cnt[state[id[i]]]=0;
}

int main(){
    memset(head,-1,sizeof head);
    cin>>n;
    for(register int i=2;i<=n;++i){
        int to; char col;
        cin>>to>>col;
        add(to,i,1<<(col-'a')),add(i,to,1<<(col-'a'));
    }
    dep[1]=1,dfs_getson(1,-1),dsu(1,-1,true);
    for(register int i=1;i<=n;++i) printf("%d ",ans[i]);
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章