1. 程式人生 > 實用技巧 >【學習筆記】樹上啟發式合併 概念+基礎運用

【學習筆記】樹上啟發式合併 概念+基礎運用

引入

現在考慮這樣一類樹上統計問題:

  • 無修改操作,詢問允許離線

  • 對子樹資訊進行統計(鏈上的資訊在某些條件下也可以統計

用什麼來做呢?暴力?樹上差分?樹上莫隊?

今天要介紹的這種演算法 將吊打以上

有請它閃亮登場——dsu on tree!!!

看到這個名字,嗯?dsu?不是並查集嘛(至少我第一眼是這麼想的),樹上並查集?
其實它和並查集沒有半毛錢關係,用dsu這個名字可能只是因為它和並查集都運用了一種思想——啟發式合併

什麼意思呢?還記得我們並查集的按秩合併嗎?

在這個合併的過程中 我們把小的集合合併到了大集合中去

同樣的 樹上啟發式合併實際就是把一棵高度小的子樹合併到了大的子樹去,這樣的一個優化。

演算法內容

先思考下面這個問題

給定一棵樹 樹的每個結點都有一個確定的顏色,詢問一些子樹的顏色數量(顏色可以重複)

我們先想想O(N²)的暴力做法

對於每個節點,暴力遍歷子樹,將它們的資料統計出來得到當前節點的答案,然後再暴力將這棵子樹的資料清空,以免影響到別的節點。

由於空間的限制,我們不可能對於每一個節點開一個數組來記錄資料,只能開一個全域性陣列。
在這個全域性陣列內,如果不清空,就會影響到別的子樹,答案混淆於是導致WA

但是可以發現,統計兒子節點時最後那個節點其實沒有必要清空,因為它不再會影響到它的兄弟節點。那麼該如何優化呢?

輕重鏈剖分優化

對於節點x,可以在做子樹答案時保留最後一棵子樹u的資料不清空

,然後統計x的答案時繞過u節點統計別的子樹。那麼u選哪個呢?注意我們是選擇一個

子樹來儲存答案,對於其他的非重兒子進行暴力,為了複雜度最優,當然是選子樹個數多的咯
於是我們可以通過重兒子進行演算法優化。

可以發現,每個節點的答案由其子樹和其本身得到,考慮利用這個性質處理問題。

首先我們可以先預處理出每個節點子樹的size和它的重兒子,重兒子同樹鏈剖分一樣,是擁有節點最多子樹的兒子,這個過程顯然可以O(n)完成

接下來,對於每一個結點,我們按照這樣的方式進行遍歷

  • 遍歷所有輕兒子,遞迴結束時消除它們的貢獻

  • 遍歷所有重兒子,保留它的貢獻

  • 再計算當前子樹中所有輕子樹的貢獻

  • 更新答案

  • 如果當前點是輕兒子,消除當前子樹的貢獻

這樣,對於一個節點,我們遍歷了一次重子樹,兩次非重子樹,顯然是最划算的。

通過執行這個過程,我們獲得了這個節點所有子樹的 ans

思路已經很清晰了,通過這樣的優化,最終的時間複雜度我們發現應該為那麼總複雜度就是 O (n log ⁡n)

最主要的原因

  • 根據樹鏈剖分相關理論,每個點到根的路徑上有logn條重鏈與logn條輕鏈
  • 即一個點的資訊只會上傳logn次(詳細證明暫且不寫,如需可查閱其他大佬證明)

大致的程式碼如下所示(隨便口糊的,不要真的去測,有個思路就好)

預處理:

inline void dfs1(int x,int fa){
    size[x]=1;
    //depth 
    for(int i=head[x];i;i=t[i].nxt){
        int to=t[i].to;
        if(to!=fa){
            dfs1(to,x);
            size[x]+=size[to];
            if(size[to]>size[son[x]])
                son[x]=to;
        }
    }
}//dfs預處理 

求ans

inline int dfs2(int u,int fa,int isson,int keep){
    if(keep){
        for(int i=head[u];i;i=t[i].nxt){
            int to=t[i].to;
            if(to!=fa&&to!=son[u]){
                dfs2(to,u,0,1);
            }
        }
    }
    int tmp=0;
    if(!keep&&son[u])tmp+=dfs2(son[u],u,1,0);
    else if(son[u]) tmp+=dfs2(son[u],u,1,1);
    for(int i=head[u];i;i=t[i].nxt){
        int v=t[i].to;
        if(v!=fa&&v!=son[u]){
            tmp+=dfs2(v,u,0,0);
        }
    }
    if(!cnt[c[u]]){
        tmp++;
    }
    cnt[c[u]]++;
    if(keep)ans[u]=tmp;
    if(keep&&!isson) memset(cnt,0,sizeof(cnt));
    return tmp;
}

練習

給一些很不錯的例題

CF600ELomsatgelral

UOJ284 快樂遊戲雞

樹上數顏色

CF208E Blood Cousins

CF570D Tree Requests

CF246E Blood Cousins Return