一、什么是最小樹形圖?
就是指有向圖上的最小生成樹,英文是Directed Minimum Spanning Tree。常用的算法是朱劉算法(也稱 Edmonds 算法),可以在O(nm)時間內(nèi)解決最小樹形圖問題。
(1)過程
對于每個點,選擇它入度最小的那條邊
如果沒有環(huán),算法終止;否則進行縮環(huán)并更新其他點到環(huán)的距離。
(2)實現(xiàn)
bool solve() { ans = 0; int u, v, root = 0; for (;;) { f(i, 0, n) in[i] = 1e100; f(i, 0, m) { u = e[i].s; v = e[i].t; if (u != v && e[i].w < in[v]) { in[v] = e[i].w; pre[v] = u; } } f(i, 0, m) if (i != root && in[i] > 1e50) return 0; int tn = 0; memset(id, -1, sizeof id); memset(vis, -1, sizeof vis); in[root] = 0; f(i, 0, n) { ans += in[i]; v = i; while (vis[v] != i && id[v] == -1 && v != root) { vis[v] = i; v = pre[v]; } if (v != root && id[v] == -1) { for (int u = pre[v]; u != v; u = pre[u]) id[u] = tn; id[v] = tn++; } } if (tn == 0) break; f(i, 0, n) if (id[i] == -1) id[i] = tn++; f(i, 0, m) { u = e[i].s; v = e[i].t; e[i].s = id[u]; e[i].t = id[v]; if (e[i].s != e[i].t) e[i].w -= in[v]; } n = tn; root = id[root]; } return ans; }
二、Tarjan 的 DMST 算法
Tarjan 提出了一種能夠在時間內(nèi)解決最小樹形圖問題的算法。
(1)過程
Tarjan 的算法分為收縮與伸展兩個過程。接下來先介紹收縮的過程。
我們需要假設(shè)輸入的圖是滿足強連通的,如果不滿足那么就加入O(n)條邊使其滿足,并且這些邊的邊權(quán)是無窮大的。
我們需要一個堆存儲結(jié)點的入邊編號,入邊權(quán)值,結(jié)點總代價等相關(guān)信息,由于后續(xù)過程中會有堆的合并操作,這里采用 左偏樹與并查集實現(xiàn)。算法的每一步都選擇一個任意結(jié)點 v,需要保證 v 不是根節(jié)點,并且在堆中沒有它的入邊。再將 v 的最小入邊加入到堆中,如果新加入的這條邊使堆中的邊形成了環(huán),那么將構(gòu)成環(huán)的那些結(jié)點收縮,我們不妨將這些已經(jīng)收縮的結(jié)點命名為超級結(jié)點,再繼續(xù)這個過程,如果所有的頂點都縮成了一個超級結(jié)點,那么收縮過程就結(jié)束了。整個收縮過程結(jié)束后會得到一棵收縮樹,之后將對它進行伸展操作。
堆中的邊總是會形成一條路徑由于圖是強連通的,這個路徑必然存在,并且其中的可能是最初的單一結(jié)點,也可能是壓縮后的超級結(jié)點。
最初有 ,其中 a 是圖中任意的一個結(jié)點,每一次選擇一條最小入邊,如果 u 不是中的一個結(jié)點,那么就將結(jié)點擴展到。如果 u 是他們其中的一個結(jié)點,那么就找到了一個關(guān)于的環(huán),再將他們收縮為一個超級結(jié)點 c。
向隊列P中放入所有的結(jié)點或超級結(jié)點,并初始選擇任意一節(jié)點 a,只要隊列不為空,就進行以下步驟:
(1)選擇 a 的最小入邊,保證不存在自環(huán),并找到另一頭的結(jié)點 b。如果結(jié)點 b 沒有被記錄過說明未形成環(huán),令a←b,繼續(xù)當前操作尋找環(huán)。
(2)如果 b 被記錄過了,就說明出現(xiàn)了環(huán)。總結(jié)點數(shù)加一,并將環(huán)上的所有結(jié)點重新編號,對堆進行合并,以及結(jié)點/超級結(jié)點的總權(quán)值的更新。更新權(quán)值操作就是將環(huán)上所有結(jié)點的入邊都收集起來,并減去環(huán)上入邊的邊權(quán)。
以圖片為例,左邊的強連通圖在收縮后就形成了右邊的一棵收縮樹,其中 a 是結(jié)點 1 與結(jié)點 2 收縮后的超級結(jié)點,b 是結(jié)點 3,結(jié)點 4,結(jié)點 5 收縮后的超級結(jié)點,A是兩個超級結(jié)點 a 與 b 收縮后形成的。
伸展過程是相對簡單的,以原先要求的根節(jié)點 r 為起始點,對 r 到收縮樹的根上的每一個環(huán)進行伸展。再以 r 的祖先結(jié)點 fr 為起始點,將其到根的環(huán)展開,直到遍歷完所有的結(jié)點。
代碼如下:
#include <bits/stdc++.h> using namespace std; typedef long long ll; #define maxn 102 #define INF 0x3f3f3f3f struct UnionFind { int fa[maxn << 1]; UnionFind() { memset(fa, 0, sizeof(fa)); } void clear(int n) { memset(fa + 1, 0, sizeof(int) * n); } int find(int x) { return fa[x] ? fa[x] = find(fa[x]) : x; } int operator[](int x) { return find(x); } }; struct Edge { int u, v, w, w0; }; struct Heap { Edge *e; int rk, constant; Heap *lch, *rch; Heap(Edge *_e) : e(_e), rk(1), constant(0), lch(NULL), rch(NULL) {} void push() { if (lch) lch->constant += constant; if (rch) rch->constant += constant; e->w += constant; constant = 0; } }; Heap *merge(Heap *x, Heap *y) { if (!x) return y; if (!y) return x; if (x->e->w + x->constant > y->e->w + y->constant) swap(x, y); x->push(); x->rch = merge(x->rch, y); if (!x->lch || x->lch->rk < x->rch->rk) swap(x->lch, x->rch); if (x->rch) x->rk = x->rch->rk + 1; else x->rk = 1; return x; } Edge *extract(Heap *&x) { Edge *r = x->e; x->push(); x = merge(x->lch, x->rch); return r; } vector<Edge> in[maxn]; int n, m, fa[maxn << 1], nxt[maxn << 1]; Edge *ed[maxn << 1]; Heap *Q[maxn << 1]; UnionFind id; void contract() { bool mark[maxn << 1]; // 將圖上的每一個結(jié)點與其相連的那些結(jié)點進行記錄。 for (int i = 1; i <= n; i++) { queue<Heap *> q; for (int j = 0; j < in[i].size(); j++) q.push(new Heap(&in[i][j])); while (q.size() > 1) { Heap *u = q.front(); q.pop(); Heap *v = q.front(); q.pop(); q.push(merge(u, v)); } Q[i] = q.front(); } mark[1] = true; for (int a = 1, b = 1, p; Q[a]; b = a, mark[b] = true) { // 尋找最小入邊以及其端點,保證無環(huán)。 do { ed[a] = extract(Q[a]); a = id[ed[a]->u]; } while (a == b && Q[a]); if (a == b) break; if (!mark[a]) continue; // 對發(fā)現(xiàn)的環(huán)進行收縮,以及環(huán)內(nèi)的結(jié)點重新編號,總權(quán)值更新。 for (a = b, n++; a != n; a = p) { id.fa[a] = fa[a] = n; if (Q[a]) Q[a]->constant -= ed[a]->w; Q[n] = merge(Q[n], Q[a]); p = id[ed[a]->u]; nxt[p == n ? b : p] = a; } } } ll expand(int x, int r); ll expand_iter(int x) { ll r = 0; for (int u = nxt[x]; u != x; u = nxt[u]) { if (ed[u]->w0 >= INF) return INF; else r += expand(ed[u]->v, u) + ed[u]->w0; } return r; } ll expand(int x, int t) { ll r = 0; for (; x != t; x = fa[x]) { r += expand_iter(x); if (r >= INF) return INF; } return r; } void link(int u, int v, int w) { in[v].push_back({u, v, w, w}); } int main() { int rt; scanf("%d %d %d", &n, &m, &rt); for (int i = 0; i < m; i++) { int u, v, w; scanf("%d %d %d", &u, &v, &w); link(u, v, w); } // 保證強連通 for (int i = 1; i <= n; i++) link(i > 1 ? i - 1 : n, i, INF); contract(); ll ans = expand(rt, n); if (ans >= INF) puts("-1"); else printf("%lld\n", ans); return 0; }
C語言網(wǎng)提供由在職研發(fā)工程師或ACM藍橋杯競賽優(yōu)秀選手錄制的視頻教程,并配有習題和答疑,點擊了解:
一點編程也不會寫的:零基礎(chǔ)C語言學練課程
解決困擾你多年的C語言疑難雜癥特性的C語言進階課程
從零到寫出一個爬蟲的Python編程課程
只會語法寫不出代碼?手把手帶你寫100個編程真題的編程百練課程
信息學奧賽或C++選手的 必學C++課程
藍橋杯ACM、信息學奧賽的必學課程:算法競賽課入門課程
手把手講解近五年真題的藍橋杯輔導(dǎo)課程