dsu on tree學習筆記

前言

一次模擬賽的\(T3\)傳送門html

只會\(O(n^2)\)的我就\(gg\)了,而且對於題解提供的\(\text{dsu on tree}\)的作法一臉懵逼。node

看網上的其餘大佬寫的筆記,我本身畫圖看了一天才看懂(我太蒻了),因而就有了這篇學習筆記。git

概念篇/基礎運用

算法簡介

如今考慮這樣一類樹上統計問題:github

  • 無修改操做,詢問容許離線算法

  • 對子樹信息進行統計(鏈上的信息在某些條件下也能夠統計)數組

樹上莫隊?點分治?數據結構

\(\text{dsu on tree}\)能夠把它們吊起來打!學習

\(\text{dsu on tree}\)運用樹剖中的輕重鏈剖分,將輕邊子樹信息累加到重鏈上進行統計,擁有\(O(nlogn)\)的優秀複雜度,常數還賊TM小,你值得擁有!優化

//雖然說是dsu on tree,但某個毒瘤@noip說這是靜態鏈分治

//還有其餘的數據結構神du仙liu說它能夠被當作是靜態的樹剖(由於其在樹上有強大的統計信息的能力,但不能支持修改操做),與正常的樹鏈剖分相對

//因此我同時保留這幾種說法,但願數據結構神du仙liu們不要噴我這個juruo

算法實現

  • 遍歷全部輕兒子,遞歸結束時消除它們的貢獻ui

  • 遍歷全部重兒子,保留它的貢獻

  • 再計算當前子樹中全部輕子樹的貢獻

  • 更新答案

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


那麼這裏有人可能就要問了,爲何不保留求出的全部答案呢?這樣複雜度就更優了啊

若是這樣的話,當你處理完一顆子樹的信息時,再遞歸去求解另外一顆子樹時,

已有的答案就會與當前子樹信息相混淆,就會產生錯誤答案。


因此,從這咱們看出,一個節點只能選擇一個子節點來保留答案

其它的都要去暴力求解

那麼選擇哪個節點能使複雜度最優呢?

顯然,咱們要儘可能均衡答案被保留的子樹和不被保存的子樹的大小

這是否是就很像樹鏈剖分劃分輕重兒子了呢?

人工圖解

由於窩太蒻了一開始沒怎麼理解它,因此有了圖解這個環節23333。

  • 好比如今有一個已經剖好的樹(粗邊爲重邊,帶紅點的是重兒子):

  • 首先,咱們先一直跳輕兒子跳到這個位置:

  • 記錄它的答案,並撤銷影響,一直往輕兒子上跳

  • 而後發現下一步只能跳到一個重兒子上,就記錄他的答案並保存(下文圖中被染色的點即爲目前保存了答案的點)

  • 接着回溯到父節點上,往下計算答案

  • 由於重兒子保存了答案被標記,往下暴力計算的時候只會通過輕邊及輕兒子(即\(6 \rightarrow 12\)這條邊和\(12\)號節點)

  • 同理,\(2\)號點也可進行相似操做,由於它的重兒子\(6\)號節點已保存了這顆子樹的答案,只需上傳便可,

    不用再從\(6\)這個位置再往下走統計答案,惟一會暴力統計答案的只有它的輕兒子\(5\)號節點

  • 而後繼續處理根節點另外一個輕兒子\(3\),一直到葉子節點收集信息

  • 最後,對根節點的重兒子進行統計,如圖,先對箭頭所指的兩個輕兒子進行計算

  • 接着對每個重兒子不斷保存答案,對輕兒子則暴力統計信息,將答案不斷上傳

  • 而後,對於根節點的處理同上便可

大體代碼:

inline void calc(int x,int fa,int val)
{
    ......................
    /*
        針對不一樣的問題
        採起各類操做
    */
    for(rg int i=0;i<(int)G[x].size();++i)
    {
        int v=G[x][i];
        if(vis[v] || v==fa) continue;
        calc(v,x,val);
    }
}
inline void dfs(int x,int fa,int keep)//keep表示當前是否爲重兒子
{
    for(int i=0;i<(int)G[x].size();++i)
    {
        int v=G[x][i].v;
        if(v==fa || v==son[x]) continue;
        dfs(v,x,0);
    }
    if(son[x]) dfs(son[x],x,1),vis[son[x]]=true;//標記重兒子
    calc(x,fa,1);vis[son[x]]=false;//計算貢獻
    ans[x]=....;//記錄答案
    if(!keep) calc(x,fa,-1);//不是重兒子,撤銷其影響
}

若是是維護路徑上的信息,大概還能夠這麼寫:(若是有錯,請大佬指出)

ps:關於\(\text{dsu on tree}\)對路徑上信息進行維護的精彩應用,能夠看最後\(3\)道例題

inline void dfs(int x,int fa)
{
    siz[x]=1,dep[x]=dep[fa]+1,nid[rev[x]=++idx]=x;
    //再次藉助樹剖的思想,子樹內節點順序轉爲線性 
    for(rg int i=0;i<(int)G[x].size();++i)
    {
        int v=G[x][i].v,w=G[x][i].w;
        if(v==fa) continue;
        dfs(v,x),siz[x]+=siz[v];
        if(!son[x] || siz[v]>siz[son[x]]) son[x]=v;
    }
}
inline void calc(int x,int val)
{//對x這一節點進行單獨處理 
    if(val>0) //計算貢獻 
    else //撤銷影響 
}
inline void dfs2(int x,int fa,int keep)
{
    for(rg int i=0;i<(int)G[x].size();++i)
    {
        int v=G[x][i].v;
        if(v==fa || v==son[x]) continue;
        dfs2(v,x,0);
    }
    if(son[x]) dfs2(son[x],x,1);
    for(rg int i=0;i<(int)G[x].size();++i)
    {
        int v=G[x][i].v;
        if(v==fa || v==son[x]) continue;
        for(rg int j=0;j<siz[v];++j)
        {
            int vv=nid[rev[v]+j]; 
            ..........
            //更新答案 
        }
        for(rg int j=0;j<siz[v];++j) calc(nid[rev[v]+j],1);
    }
    calc(x,1);
    ..........//更新答案 
    if(!keep) for(rg int i=0;i<siz[x];++i) calc(nid[rev[x]+i],-1);
}

複雜度證實

不感興趣的大佬能夠跳過這一段。(蒟蒻本身亂\(yy\)的證實,若是有錯請大佬指出)

  • 顯然,根據上面的圖解,一個點只有在它到根節點的路徑上遇到一條輕邊的時候,本身的信息纔會被祖先節點暴力統計一遍

  • 而根據樹剖相關理論,每一個點到根的路徑上有\(logn\)條輕邊和\(logn\)條重鏈

  • 即一個點的信息只會上傳\(logn\)

  • 若是一個點的信息修改是\(O(1)\)的,那麼總複雜度就是\(O(nlogn)\)

幾道可愛的例題

例題\(1\)\[\color{#66ccff}{\texttt{-> 樹上數顏色 <-}}\]

此題來自洛咕日報第\(65\)做者\(\text{codesonic}\)


  • 咱們能夠維護一個全局數組\(cnt\),表明正在被計算的子樹的每種顏色的數量

  • 每次計算子樹貢獻的時候,把節點信息往裏面加就好了,若是一個顏色第一次出現,則顏色種類數\(top++\)

  • 對於須要撤銷影響的子樹,把信息從裏面丟出來便可,若是被刪除的顏色只有這一個,則顏色種類數\(top--\)

\(Code\)

例題\(2\)\[\color{#66ccff}{\texttt{-> CF600E Lomsat gelral <-}}\]

公認\(\text{dsu on tree}\)模板題,相比於上題只是增長了對每種數量的顏色和的統計。

  • 咱們能夠維護\(cnt\)數組,表示某個顏色出現的次數;再維護一個\(sum\)數組,表示當前子樹出現了\(x\)次的顏色的編號和

  • 對節點信息統計時,先把它在\(sum\)數組裏的貢獻刪掉,更新了\(cnt\)數組後再添回去

  • 而後別忘了開\(long \, long\)血的教訓

\(Code\)

應用篇/各類靈活運用

CF570D Tree Requests

\[\color{orange}{\texttt{-> 原題傳送門 <-}}\]


窩太菜了,不會二進制優化,只會\(O(26*nlogn)\)

  • 首先,由於要造成迴文串、又能夠對字符進行任意排列,因此最多隻能有一種字母的出現次數爲奇數

  • 而後咱們維護一個\(cnt\)數組,統計每一個深度全部字母的出現次數:

cnt[dep[x]][s[x]-'a']+=val;
  • 最後再\(check\)一下就行了

\(Code\)

CF246E Blood Cousins Return

\[\color{orange}{\texttt{-> 原題傳送門 <-}}\]


  • 首先用\(map\)把給的全部名字哈希成\(1\)\(n\)的數字

  • 題目就能夠轉化爲求出每一個深度有多少不一樣的數

  • 一樣,對每一個深度開個\(set\)去重並統計

  • 而後就是套板子的事情了

\(Code\)

CF208E Blood Cousins

\[\color{orange}{\texttt{-> 原題傳送門 <-}}\]


  • 顯然原問題能夠轉化爲求該點的\(k\)級祖先有多少個\(k\)級兒子(若是沒有\(k\)級祖先,答案就是0)

  • 而一個點\(x\)\(k\)級兒子即爲在以\(x\)爲根節點的子樹中有多少點的\(dep\)\(dep[x] + k\)

  • 把全部詢問讀進來,求出相關的點的\(k\)級祖先(能夠離線\(O(n)\)處理,也能夠倍增\(O(nlogn)\)搞;若是時空限制比較緊,就採起前者吧)

  • 而後由於是統計節點數,因此開一個普通的\(cnt\)數組維護便可。最後答案別忘了\(-1\),由於算了本身

扔一個增強版的(\(N \le 10^6\)\(128MB,1s\)):\(\color{#66ccff}{\texttt{-> 傳送門 <-}}\)

友情提醒:上面這道良心題不只卡空間,還卡時間(若是你用dsu on tree)

\(Code\)

IOI2011 Race

\[\color{orange}{\texttt{-> 原題傳送門 <-}}\]


點分治的題怎麼能用點分治呢?並且這仍是dsu on tree學習筆記

  • 首先,這道題是對鏈的信息進行統計,就不能再像對子樹的統計方法去搞♂了,因此須要一些奇技淫巧

  • 思路與點分治同樣,對於每一個節點\(x\),統計通過\(x\)的路徑的信息

  • 注意到這道題鏈上的信息是可加減的,因此咱們能夠不保存\(x\)的子孫\(\rightarrow x\)的信息,而是保存每一個節點到根節點的信息,在統計的時候在減去\(x \rightarrow\)根節點的信息

  • 而後咱們考慮如何統計,咱們能夠在每一個節點維護一個桶\(cnt\),記錄從這個點\(x\)往下走的全部路徑中,能造成的每種路徑權值和以及其所須要的最少的邊的數量:

Code

  • 對於\(v_{ij}\),計算出其到\(x\)的距離\(dis\)及深度差\(d\)(能夠當作路徑上的節點數),並用\(d\) \(+\) \(cnt[\)k−dis\(]\)來更新答案。

  • 而後用剛纔獲得的\(dis\)對應的\(d\)來更新\(cnt[dis]\)的值。

  • 這樣就至關於,用每一個\(v_{ij}\)\(x\)的鏈,與以前桶中所保存某條鏈的路徑權值和之和恰爲\(k\)的拼成一條路徑,並更新答案。而後,再把它也加入桶中

  • 再套上\(\text{dsu on tree}\)的板子,每一個節點保存它的重兒子的 桶的信息便可

雖然是\(O(nlog^2n)\)的,但常數小,咱不慌

可是窩太菜了,用\(map\)做桶不開\(O2\)\(T \, 3\)個點(畢竟用了\(STL\),還有兩隻\(log\)),有空再重寫一遍233

貌似用\(unodered_{}map\)不開\(O2\)也卡得過去。。

\(Code\)

NOIP2016 每天愛跑步

\[\color{orange}{\texttt{-> 原題傳送門 <-}}\]


  • 首先,咱們能夠把\(S \Rightarrow T\)這條路徑拆成\(S \rightarrow lca(S,T)\)\(lca(S,T) \rightarrow T\)兩段來考慮

  • 考慮在第一段路徑上一點\(u\)能觀測到該玩家的條件是:\(dep[S] - dep[u] = w[u]\)

  • 同理,在第二段路徑上一點\(u\)能觀測到該玩家的條件是:\(dep[T] - dep[u] = dis(S,T) - w[u]\),即\(dep[S] - 2 \times dep[lca(S,T)] = w[u] - dep[u]\)

  • 而後能夠用差分的思想,對每一個節點開兩個桶\(up\)\(down\)進行統計

  • \(S\)\(up\)中插入\(dep[S]\)

  • \(T\)\(down\)中插入\(dep[S] - 2 \times dep[lca(S,T)]\)

  • 由於\(lca(S,T)\)會對\(S \rightarrow T\)\(T \rightarrow S\)都進行統計,因此在其\(up\)中刪除\(dep[S]\)

  • 同理,在\(fa[lca(S,T)]\)\(down\)中刪除\(dep[S] - 2 \times dep[lca(S,T)]\)

  • 而後用\(\text{dsu on tree}\)統計便可,答案爲\(up[w[u]+dep[u]] + down[w[u] - dep[u]]\)

注意到\(w[u] - dep[u]\)可能小於零,爲了不負數下標、又不想套\(map\),咱們可使用以下\(trick\)

int up[N],CNT[N<<1],*down=&CNT[N];
//把donw[0]指向CNT[N],這樣就能夠給負數和正數都分配大小爲N的空間

跑的雖然沒有普通的差分快,不過吊打線段樹合併仍是綽綽有餘的

\(Code\)

[Vani有約會]雨天的尾巴

\[\color{orange}{\texttt{-> 原題傳送門 <-}}\]

跟每天愛跑步差很少,就不畫圖了(~懶)

  • 同上題,用差分的思想,對每一個節點的增長和刪除開兩個桶統計

  • 同時,這題要維護每一個點出現的最多物品的種類,直接開個線段樹維護就行了

\(O(nlog^2n)\),常數應該和樹剖差很少,不過由於每一個點都要進行增長刪除兩個操做,常數大了一倍,並且還用了線段樹,因此\(\cdots\)

不過依然比部分線段樹合併跑的快2333

\(Code\)

由以上三題,咱們能夠看出,在必定條件下,\(\text{dsu on tree}\)也是能夠在鏈上搞♂事情的

好比\(Race\)知足鏈上信息可加減性,後兩道題能夠用差分將鏈上的修改/詢問轉化爲點上的修改/詢問

\(\text{dsu on tree}\)能夠應用的條件確定不止以上兩種,由於窩太蒻了,只見識了這些題,之後看到其餘類型的也會補上來

射手座之日

\[\color{orange}{\texttt{-> 提交地址 <-}}\]


如今終於能夠回過頭來解決這個題了

留給你們思考吧,要代碼的話能夠私信我

雖然有不少大佬會線段樹合併或虛樹上\(dp\)秒切這道題,不過仍是但願用\(dsu \; AC\)

參考資料/總結

參考資料

總結

之後還會不按期地添加\(\text{dsu on tree}\)的相關題目~

若是有須要,我會把最後那道題的代碼貼出來

相關文章
相關標籤/搜索