雖然叫dsu但這和並查集貌似沒什麼關係
例:當我們需要在每個子樹上統計一些資訊的時候,往往會開乙個全域性的cnt陣列,試圖 dfs \(o(n)\) 掃一遍,一邊加點一邊得到答案給你一棵樹,每個節點有乙個顏色,要求出每個子樹中數量最多的顏色並輸出
(數量相同的情況先不考慮 不重要)
但對於一棵樹而言顯然有問題:當我們統計完其左子樹的資訊後,必須清空整個cnt陣列才能去掃右子樹,這樣其實就已經變成 \(o(n^2)\) 了
當然我們可以稍微偷工減料一點,因為最後一棵子樹統計完後不用清空,我們可以最後遍歷最大的那棵子樹
最大子樹可以通過一遍dfs預處理出子樹的size,記錄每個點的重兒子得到(類似樹剖)
然而就是這一點偷工減料,使得整個演算法複雜度直接降至 \(o(nlogn)\)
如果不關心證明的話,你已經學會 dsu on tree 了
證明為什麼這樣瞎搞就能獲得\(nlogn\)的複雜度:
以下通過感性理解的方式說明為什麼這東西能優化這麼多
回顧一下在每個節點處我們要做什麼:
前兩步的操作一共是 \(o(n)\) 的,就是最樸素的從頭到尾掃一遍
現在需要考慮:在每個點處對每個輕子樹掃一遍的複雜度
如果乙個點和根節點之間一共有 x 條輕邊,那麼它會被遍歷差不多 x+1 次
而輕重鏈剖分有個很好的性質:走一條輕邊時,節點數量至少被砍一半,否則這就不是輕邊了
那麼從根節點到任意節點經過的輕邊數量最多是 \(logn\) 級別的
所以其實很顯然了:複雜度就是 \(o(nlogn)\)
再看看極端情況加深理解:
樹上問題最容易被出題人的各種鏈,菊花圖,鏈加菊花圖啥的卡掉
如果這棵樹長得像鏈,它將被最後走最大子樹這一小貪心優化掉一大半;
如果這棵樹長得像菊花圖,,那麼根節點到任意節點間的輕邊數量都將是極少的;
所以你可以相信dsu on tree
**(這道題的)
int n;
int col[maxn];
int cnt[maxn];
ll ans[maxn];
int siz[maxn], son[maxn];
struct edgee[maxn*2];
int hd[maxn], ecnt = 0;
inline void add(int x, int y)
void dfs1(int p, int fa)
}}ll tot = 0, mxc = 0;
void addcol(int c, int ad)else if(cnt[c] == mxc)
}void cntall(int p, int fa, int d)
}addcol(col[p], d);
}void dfs(int p, int fa, int s**)
}if(son[p]) dfs(son[p], p, 1);
for(int i=hd[p];i;i=e[i].nt)
}//此時所有子節點均已記錄
addcol(col[p], 1);
ans[p] = tot;
if(!s**) cntall(p, fa, -1), tot = mxc = 0;
}void solve()
dfs1(1, -1);
dfs(1, -1, 1);
for(int i=1;icout << ans[n] << '\n';
}
樹上啟發式合併
解決樹上統計問題,o n log n o n log n o n lo g n 可以結合線段樹等資料結構維護深度上的資訊 部落格 入門題 const int maxn 1e5 7 const int mod 1e9 7 ll n,m,u,v,mx,sum vector int mp maxn int...
樹上啟發式合併
樹上啟發式合併,一種美妙的黑科技,可以用普通的優化讓你 n 2 變成嚴格 n log 解決一些類似 樹上數顏色,樹上查眾數 這樣的問題 首先你要知道暴力為什麼是 n 2 的 以這個圖為例 每次你從乙個節點開始向下搜,你從1節點搜到3,搜完這個子樹然後你需要把3存的col等資訊刪去再遍歷另乙個子樹才是...
樹上啟發式合併總結
某一天發現一道樹上啟發式合併裸題,但我不會寫 學習並刷了兩天的題,是時候來寫個總結了 樹上啟發式合併 dsu on tree 是乙個在o n logn o nlogn o nlog n 時間內解決許多樹上問題的有力演算法。但它的中心其實是 暴力!沒錯,它正是由暴力優化而來。我們先看一道例題 cf60...