Dsu on tree
阿新 • • 發佈:2020-11-16
Dsu on tree
簡介
Dsu on tree(樹上啟發式合併)是一種統計樹上一個節點的子樹中具有某種特徵的節點數的演算法 如CF 600E 。本質上是利用輕重鏈剖分對爆搜優化。
如果一個問題具有如下性質:
1.只有對子樹的查詢
2.沒有修改(靜態)
基本上就可以直接懟Dsu on tree了。
演算法講解
看問題例項:
給出一棵樹,每個節點有一種顏色。
求每個節點以當前節點為根的子樹中出現次數最多的顏色的編號和。
(其實就是CF600E)
首先想想如何爆搜:
遍歷每個節點,每個節點統計一遍顏色有多少個。完事之後再消除當前節點的貢獻,繼續遞迴。
每個節點都要遍歷一遍子樹,時間複雜度為\(O(n^2)\)
顯然不夠優秀。
那麼我們來看看它都做了些什麼無用功
對於最後一次搜尋,它的結果是不用清空的,因為它的答案可以用於父節點答案統計。
那我們可以試著留住儘量大的子樹,也就是重兒子,那麼就樹剖一遍求得重兒子,回溯時不擦除就行了。
關於時間複雜度,因為每個點的輕邊只有 \(O(log\ n)\) 條,故時間複雜度為 \(O(nlog\ n)\)。
這裡Orz一下發明人,把暴力玩到這麼優雅( (
核心程式碼模板:
void dfs(int x,int f,int p) /*x:當前節點 f:當前節點父節點 p:當前節點是否需要保留*/ { for(/*遍歷所有相鄰節點*/) { int y=/*遍歷到的節點*/ if(/*y不是重兒子或父節點*/) dfs(y,x,0); } if(/*當前節點有重兒子*/) dfs(/*重兒子*/,x,1),Son=/*重兒子編號*/; //統計重兒子,不消除影響 /*統計所有輕兒子的答案*/ /*更新答案並刪除貢獻*/ }
CF600E code:
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int N=1e5+10; int head[N],ver[N<<1],nxt[N<<1],tot=0; void add(int x,int y) { ver[++tot]=y; nxt[tot]=head[x]; head[x]=tot; } int n; int col[N]; int son[N],size_[N],cnt[N]; int maxn,Son; ll sum=0,ans[N]; void dfs1(int x,int f)//輕重鏈剖分 { size_[x]=1; for(int i=head[x];i;i=nxt[i]) { int y=ver[i]; if(y==f) continue; dfs1(y,x); size_[x]+=size_[y]; if(size_[son[x]]<size_[y]) son[x]=y; } } void add_(int x,int f,int val)//統計答案 /*val=1 統計答案 val=-1 刪去答案*/ { cnt[col[x]]+=val; if(cnt[col[x]]>maxn) maxn=cnt[col[x]],sum=col[x]; else if(cnt[col[x]]==maxn) sum+=col[x]; for(int i=head[x];i;i=nxt[i]) { int y=ver[i]; if(y==f||y==Son) continue;//重兒子不管 add_(y,x,val); } } void dfs2(int x,int f,int p)/*p=0 需要消除影響*/ { for(int i=head[x];i;i=nxt[i]) { int y=ver[i]; if(y==f) continue; if(y!=son[x]) dfs2(y,x,0); } if(son[x]) dfs2(son[x],x,1),Son=son[x];//統計重兒子,不消除影響 add_(x,f,1),Son=0;//統計所有輕兒子的貢獻 ans[x]=sum; //更新答案 if(!p) add_(x,f,-1),sum=0,maxn=0;//刪除貢獻 } int main() { scanf("%d",&n); for(int i=1;i<=n;i++) scanf("%d",col+i); for(int i=1;i<n;i++) { int x,y; scanf("%d%d",&x,&y); add(x,y); add(y,x); } dfs1(1,1); dfs2(1,1,0); for(int i=1;i<=n;i++) { printf("%lld ",ans[i]); } return 0; }