【知識】圖論 朱劉演算法梳理

Star_F發表於2024-12-01

朱劉演算法:

樹形圖的定義:

  • 以某一個點為根的有向樹,被稱為 樹形圖

  • 一個有向圖,滿足無環且每個點的入度為 \(1\) (除了根節點),被稱為 樹形圖

  • 最小樹形圖:對於所有樹形圖中,找到一個總權值和最小的樹形圖,被稱為 最小樹形圖

最小樹形圖問題本質上其實就是有向圖上的最小生成樹問題。 Prim 演算法和 Kruskal 演算法可以解決無向圖上的最小生成樹問題。 朱劉演算法可以解決有向圖上的最小生成樹問題。

朱劉演算法

朱劉演算法是一個迭代演算法,每一次迭代:

  1. 除了根節點,對於每個點,找一下這個點的入邊中權值最小的邊

  2. 判斷選出的邊中是否存在環

    1. 無環,演算法結束

    2. 有環,進入步驟 \(3\)

    3. 將所有環縮點,得到新圖 \(G’\),對於每條邊:

      • 環內部的邊,刪去
      • 邊的終點 \(u\) 在環內,該邊的權值變成原權值減去 \(u\) 在環內的邊的權值,即 \(w - w_{環}\)
      • 其他邊,不變

演算法結束後,之前每一次迭代選出的所有邊的總權值之和就是答案。

證明朱劉演算法

  • 如果第一次選出的邊中不存在環,就意味著當前選出的邊滿足兩個條件,無環且每個點都有一個入邊,說明我們選出的是一個樹形圖,由於每個點選出的邊都是所有入邊裡面權值最小的邊,所以一定不可能存在其他方案使得我們選擇的邊權和更小。

  • 如果有環,那麼為什麼演算法還是對的呢?

假設原圖是 \(G\),而縮完點且更新完邊權之後的圖是 \(G’\)

我們考慮 \(G\) 中任意一個環,由於最終圖中一定不能存在環,所以這個環一定存在兩個性質,第一點是至少需要去掉一條邊,第二點是必然存在一個最優解只去一條邊。

假設我們現在任意去掉一條邊 \(a\),那麼必然會選一條新邊 \(b\) 連向 \(a\) 的終點,此時如果我們在任意去掉另外一條邊 \(c\),那麼必然會再選一條新邊 \(d\) 連向 \(c\) 的終點。此時由於 \(c\) 一定小於等於 \(d\),並且由於去掉了 \(a\),選上 \(c\) 並不能讓圖中存在環且權值會變小,因此我們一定可以把 \(d\) 換回 \(c\)。按照這個原理,任意給我們一個最優解,如果最優解中去掉的邊數大於 \(1\),那麼我們必然可以從新加的邊去掉,換回環上的邊,這樣它仍然滿足是一個樹形圖,但是總邊權和不會變大。由此得出對於任意一個環,必然存在一個最優解只去一條邊。

有了以上兩個性質,我們可以進行證明。

假設圖 \(G\) 中所有環裡面只去掉環上一條邊的樹形圖的集合放在左邊,將 \(G’\) 裡面所有樹形圖的集合放在右邊。

對於左邊集合中的圖的任何一個環,我們只去掉一條邊,然後連上一條新邊,由於環已經被我們縮點,那麼新邊就會連向縮點後的新點,對於新點而言,入邊就是唯一的,所以去掉一條邊後圖中無環且每個點的入度為 \(1\),所以去掉一條邊後會構成一個樹形圖,說明左邊集合的任意一個圖,我們都可以轉化成右邊集合的一個樹形圖。

反過來,對於右邊集合中任意一個樹形圖,我們找到一個不是根節點的縮點後的點,那麼這個點必然存在一個入邊,且這個入邊必然是原圖裡的某一條邊,且它一定連向縮點後這個點內部的某一個點,我們將這個點對應的內部的邊去掉,將這條原圖中的邊加上。這樣可以發現,任給我們一個右邊集合的樹形圖,我們都可以轉化成左邊集合的一個滿足兩個性質的樹形圖。

因此兩個集合是相互對應的。

然後看一下數量關係,可以發現左邊集合加上了一條環外邊 \(w\),去掉了一條環內邊 \(w’\),因此整個操作等於是加上了 \(w-w’\),而右邊集合中我們定義每條邊就是 \(w-w’\),所以兩個集合在數量關係上也是完全一樣的。

綜上所述,我們想求左邊集合的最小樹形圖只需要求右邊集合的最小樹形圖就行了,因此每次圖中有環進行的處理是正確的。

每次迭代最多去掉一個點,最多迭代 \(n\) 次,每次迭代內部是時間複雜度是 \(O(m)\),因此整個演算法時間複雜度是 \(O(nm)\)

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
 
typedef long long LL;
 
const int N = 105, M = 10005, INF = 1e9;
 
int n, m, rt = 1, X[N], Y[N], col, in[N];
 
int vis[N], id[N], pre[N];
 
struct E{
	int u, v, w;
} e[M];
 
int inline edmonds() {
	int ans = 0;
	while (true) {
		for (int i = 1; i <= n; i++) in[i] = INF;
		memset(vis, 0, sizeof vis);
		memset(id, 0, sizeof id);
		for (int i = 1; i <= m; i++) 
			if (e[i].w < in[e[i].v]) in[e[i].v] = e[i].w, pre[e[i].v] = e[i].u;
		for (int i = 1; i <= n; i++)
			if (in[i] == INF && i != rt) return -1;
		col = 0;
		for (int i = 1; i <= n; i++) {
			if (i == rt) continue;
			ans += in[i];
			int v = i;
			while (!vis[v] && !id[v] && v != rt)
				vis[v] = i, v = pre[v];
 			if (v != rt && vis[v] == i) {
 				id[v] = ++col;
 				for (int x = pre[v]; x != v; x = pre[x]) id[x] = col;
 			}
		}
		if (!col) break;
		for (int i = 1; i <= n; i++) if (!id[i]) id[i] = ++col;
		int tot = 0;
		for (int i = 1; i <= m; i++) {
			int a = id[e[i].u], b = id[e[i].v];
			if (a == b) continue;
			e[++tot] = (E) { a, b, e[i].w - in[e[i].v] };
		}
		m = tot, n = col, rt = id[rt];
	}
	return ans;
}
 
int main() {
	scanf("%d%d%d", &n, &m, &rt);
	int tot = 0;
	for (int i = 1; i <= m; i++) {
		int a, b, c; scanf("%d%d%d", &a, &b, &c);
		if (b != rt && a != b) e[++tot] = (E) { a, b, c };
	}
	m = tot;
	printf("%d\n", edmonds());
	return 0;
}

相關文章