1. 程式人生 > >樹上啟發式合併總結

樹上啟發式合併總結

前言

某一天發現一道樹上啟發式合併裸題,但我不會寫……
學習並刷了兩天的題,是時候來寫個總結了

正文

樹上啟發式合併(DSU on Tree),是一個在 O ( n l o g n

) O(nlogn) 時間內解決許多樹上問題的有力演算法。
但它的中心其實是——暴力!
沒錯,它正是由暴力優化而來。
我們先看一道例題:CF600E Lomsat gelral
題意簡述:一棵樹有n個結點,每個結點都是一種顏色,每個顏色有一個編號,求樹中每個子樹的最多的顏色編號的和。
我們先思考暴力:對於每個節點,暴力遍歷子樹,將它們的資料統計出來得到當前節點的答案,然後再暴力將這棵子樹的資料清空,以免影響到別的節點。
很明顯,這個做法是 O
( n 2 ) O(n^2)
的。

有的同學可能會有疑問:為什麼要清空呢?
   \;


由於空間的限制,我們不可能對於每一個節點開一個數組來記錄資料,只能開一個全域性陣列。
在這個全域性陣列內,如果不清空,就會影響到別的子樹,於是導致答案錯誤。
然而可以發現,統計兒子節點時最後那個節點其實沒有必要清空,因為它不再會影響到它的兄弟節點。
這也正是接下來要講到的優化方法。

思考優化:對於節點x,可以在做子樹答案時保留最後一棵子樹v的資料不清空,然後統計x的答案時繞過v節點統計別的子樹。那麼v選哪個呢?當然是選size最大的。
於是,我們得到了一個優化後的做法:對於節點x,先統計輕兒子的答案,並將它們的資料清除;然後統計重兒子的答案,保留資料;最後遍歷其他輕兒子及其子樹,把它們的資料與重兒子合併。
非常神奇的是,經過分析,可以證明它的複雜度是 O ( n l o g n ) O(nlogn) 的!(然而我不會證明,也懶得學)
回到例題,這正是可以用這種方法簡單解決的。放程式碼:

bool s[sz];//是否是重兒子
int cnt[sz];//每種顏色出現次數
ll sum[sz],top;//每個次數之和,以及最多的次數
void add(int c,int t)
{
    sum[cnt[c]]-=c;//原來的減去
    cnt[c]+=t;
    sum[cnt[c]]+=c;//新的加上
    if (sum[top+1]) ++top;//更新最大值
    if (!sum[top]) --top;
}
void add(int x,int fa,int t)
{
    add(col[x],t);//更新資料
    go(x) if (v!=fa&&!s[v]/*繞過重兒子*/) add(v,x,t);
}
ll ans[sz];
void dfs(int x,int fa,bool keep)//keep:是否保留當前子樹的資料
{
    go(x) if (v!=fa&&v!=son[x]) dfs(v,x,0);//遍歷輕兒子,不保留資料
    if (son[x]) dfs(son[x],x,1);//遍歷重兒子,保留資料
    s[son[x]]=1;//標記重兒子
    add(x,fa,1);//把輕兒子與重兒子的資料合併
    ans[x]=sum[top];
    s[son[x]]=0;//取消標記
    if (!keep) add(x,fa,-1);//若不保留則暴力刪除資料(相當於memset,但memset太慢)
}

練習

經過上面的講解,相信各位已經大概明白了樹上啟發式合併的思路。接下來還有幾道練習題(地址均為洛谷題庫,要去原OJ請通過洛谷上的連結過去):
CF570D Tree Requests
CF208E Blood Cousins
CF246E Blood Cousins Return
CF1009F Dominant Indices
CF375D Tree and Queries
CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths
洛谷上均有對應的樹上啟發式合併題解,有一些是我的,可以點贊。(不要臉地騙個贊)
如果還不太明白,可以去這個部落格看。(然而是英文的)

完結撒花!!