1. 程式人生 > 其它 >Kruskal重構樹 學習筆記

Kruskal重構樹 學習筆記

技術標籤:OI技巧

Kruskal重構樹 學習筆記

文章目錄

前言

Kruskal重構樹是一種比較冷門的演算法,但在解決某些問題時相當好用。

例題1 BZOJ3732 Network

連結

在一個 n n n m m m 邊的無向連通圖中多次詢問兩點間的最長邊最小值(即兩點間的瓶頸)。

這是一個經典的 Kruskal重構樹問題。

這個問題其實也可以直接用最小生成樹來解決。因為要最小化最長邊,選擇最小生成樹上的邊肯定是不劣的。於是我們可以在最小生成樹上倍增得到答案。

而Kruskal重構樹則是這樣的做的:在用Kruskal合併連通塊時,順便維護一個樹的結構。比如要合併連通塊 f u fu fu f v fv fv,中間有 權值為 w w w 的邊相連,就新建一個節點作為 合併後連通塊的根,賦予點權為 w w w,並且設定左右兒子為 f u fu fu f v fv fv。Kruskal做完後,我們就構造了具有以下特徵的樹:

  1. 葉子節點是原圖的 n n n 個點,沒有點權。
  2. 非葉子節點代表著原圖中 在生成樹上的 n − 1 n-1 n1 條邊,點權就是原圖的邊權。
  3. 由於合併時邊權有一定次序,我們發現得到的樹是個二叉堆(不考慮葉子節點)。如果是做最小(大)生成樹,得到的是大(小)根堆。
  4. 兩點於樹上的路徑上的各點,相當於原圖上兩點走最小(大)生成樹的路徑上的各邊。

這個樹叫做 Kruskal重構樹。易得這棵樹一共有 2 n − 1 2n-1 2n1 個點, 2 n − 2 2n-2 2n2 條邊。


回到這題。很容易知道,我們要找的,就是在 Kruskal 重構樹上兩點路徑中的最長邊。或者準確點,兩點路徑上的點權最大者。別犯渾用倍增做啊!由於 Kruskal 重構樹是個堆,點權最大的就是兩點的 lca 了。

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace
std; typedef long long ll; char In[1 << 20], *ss = In, *tt = In; #define getchar() (ss == tt && (tt = (ss = In) + fread(In, 1, 1 << 20, stdin), ss == tt) ? EOF : *ss++) ll read() { ll x = 0, f = 1; char ch = getchar(); for(; ch < '0' || ch > '9'; ch = getchar()) if(ch == '-') f = -1; for(; ch >= '0' && ch <= '9'; ch = getchar()) x = x * 10 + int(ch - '0'); return x * f; } const int MAXN = 30005; const int MAXM = 60005; int n, m, k, num, upto[MAXN], val[MAXN], head[MAXN], ver[MAXM], nxt[MAXM], cnt, dep[MAXN], sz[MAXN], fa[MAXN], son[MAXN], top[MAXN]; struct Edge {int u, v, w;}e[MAXM]; bool cmp(const Edge& a, const Edge& b) {return a.w < b.w;} int getup(int u) {return u == upto[u] ? u : upto[u] = getup(upto[u]);} void addedge(int u, int v) {ver[++cnt] = v; nxt[cnt] = head[u]; head[u] = cnt;} void Kruskal() { num = n; sort(e + 1, e + 1 + m, cmp); for(int i = 1; i <= n; i++) upto[i] = i; for(int i = 1; i <= m; i++) { int fu = getup(e[i].u), fv = getup(e[i].v); if(fu == fv) continue; val[++num] = e[i].w; upto[num] = upto[fu] = upto[fv] = num; addedge(fu, num); addedge(num, fu); addedge(fv, num); addedge(num, fv); } } void dfs1(int u, int f) { dep[u] = dep[f] + 1; sz[u] = 1; fa[u] = f; son[u] = 0; for(int i = head[u]; i; i = nxt[i]) if(ver[i] != f) { int v = ver[i]; dfs1(v, u); sz[u] += sz[v]; if(sz[v] > sz[son[u]]) son[u] = v; } } void dfs2(int u, int tprt) { top[u] = tprt; if(son[u]) dfs2(son[u], tprt); for(int i = head[u]; i; i = nxt[i]) if(ver[i] != fa[u] && ver[i] != son[u]) { int v = ver[i]; dfs2(v, v); } } int Lca(int u, int v) { while(top[u] != top[v]) { if(dep[top[u]] < dep[top[v]]) swap(u, v); u = fa[top[u]]; } return dep[u] > dep[v] ? v : u; } int main() { n = read(); m = read(); k = read(); for(int i = 1; i <= m; i++) e[i].u = read(), e[i].v = read(), e[i].w = read(); Kruskal(); int rt = getup(1); dfs1(rt, 0); dfs2(rt, rt); for(int i = 1; i <= k; i++) { int a = read(), b = read(); printf("%d\n", val[Lca(a, b)]); } return 0; }

例題2 [NOI2018] 歸程

連結

這道題與 Kruskal重構樹無關的部分我直接寫了篇題解,這裡僅提一下這樣一個小問題:

在一個無向連通圖中多次詢問,每次給出點 v v v 和限制 p p p, 請回答僅經過邊權 ≤ p \le p p 的邊可以到達點的個數。(和原題稍有轉化)

這是另一個 Kruskal 重構樹的模型。

這個問題似乎不好做,不過有個簡單易行的離線做法。按限制把詢問升序排序,在做最小生成樹的同時回答詢問即可。

線上做法?我只知道Kruskal重構樹,可能是我太菜

我們建完Kruskal重構樹後,問題就轉化為:不經過點權 > p >p >p 的點,最多可以到多少個點。然後由於它是一棵樹,我們能去的點都在一個子數內。倍增找到深度最小的 點權 ≤ p \le p p 的點,詢問子樹資訊即可。