前言
一次模擬賽的\(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}\)的相關題目~
如果有需要,我會把最后那道題的代碼貼出來