dsu on tree學習筆記


前言

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

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

看網上的其他大佬寫的筆記,我自己畫圖看了一天才看懂(我太蒻了),於是就有了這篇學習筆記。

概念篇/基礎運用

算法簡介

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

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

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

樹上莫隊?點分治?

\(\text{dsu on tree}\)可以把它們吊起來打!

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

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

//還有其他的數據結構神du仙liu說它可以被看成是靜態的樹剖(因為其在樹上有強大的統計信息的能力,但不能支持修改操作),與正常的樹鏈剖分相對

//所以我同時保留這幾種說法,希望數據結構神du仙liu們不要噴我這個juruo

算法實現

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

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

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

  • 更新答案

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


那么這里有人可能就要問了,為什么不保留求出的所有答案呢?這樣復雜度就更優了啊

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

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


所以,從這我們看出,一個節點只能選擇一個子節點來保留答案

其它的都要去暴力求解

那么選擇哪一個節點能使復雜度最優呢?

顯然,我們要盡量均衡答案被保留的子樹和不被保存的子樹的大小

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

人工圖解

因為窩太蒻了一開始沒怎么理解它,所以有了圖解這個環節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\)往下走的所有路徑中,能形成的每種路徑權值和以及其所需要的最少的邊的數量:

  • 對於\(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}\)的相關題目~

如果有需要,我會把最后那道題的代碼貼出來


免責聲明!

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



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