什么是最小树形图
无向图上有最小生成树,那么有向图上呢?有向图上的最小生成树称为 “最小树形图”,英文叫 Directed Minimum Spanning Tree。
如果把一个树形图的有向边替换成无向边,它会变成一棵生成树。但与生成树不同的是,树形图中会确定一个根,它必须满足根能够到达每个结点。最小树形图是所有树形图中边权和最小的一个。
朱 - 刘算法简介
一般来讲在图中求最小树形图时会确定一个根结点。有一个时间复杂度为 $O(nm)$ 的算法可以解决这个问题:朱 - 刘算法(英文 Chu - Liu Algorithm),是两个中国人一起发明的算法。
朱 - 刘算法的基本思想是每次贪心地选出每个点最小的父亲,得出的图如果不是树形图,那么将选出的环进行缩点,对边权进行修改,然后迭代这个过程,直到图变为最小树形图为止。
朱 - 刘算法模版
建议看一下「模板」最小树形图(Luogu 4716) 这题的题解,里面有确定根结点的最小树形图详细的讲解。注意大部分题解的时间复杂度都是 $O(n^3)$ 而不是 $O(nm)$ 的,问题出在它们找环的过程不能保证复杂度。如果不会找环的话可以看看「NOIP 2015」信息传递(Luogu 2661)这题,题解中的复杂度都是正确的。
1 |  | 
2 | |
3 | const int maxn = 100, maxm = 1e4, inf = 1e9 + 1; | 
4 | int n, m, rt, mn[maxn + 3], fa[maxn + 3], bel[maxn + 3], id[maxn + 3]; | 
5 | |
6 | struct edge { | 
7 | 	int u, v, w; | 
8 | } e[maxm + 3]; | 
9 | |
10 | int chu_liu() { | 
11 | 	int ans = 0; | 
12 | 	while (true) { | 
13 | 		for (int i = 1; i <= n; i++) { | 
14 | 			mn[i] = inf, fa[i] = 0, bel[i] = 0, id[i] = 0; | 
15 | 		} | 
16 | 		for (int i = 1; i <= m; i++) if (e[i].u != e[i].v) { | 
17 | 			if (e[i].w < mn[e[i].v]) { | 
18 | 				mn[e[i].v] = e[i].w, fa[e[i].v] = e[i].u; | 
19 | 			} | 
20 | 		} | 
21 | 		for (int i = 1; i <= n; i++) { | 
22 | 			if (i != rt && !fa[i]) return -1; | 
23 | 		} | 
24 | 		int cnt = 0; | 
25 | 		for (int i = 1; i <= n; i++) if (i != rt) { | 
26 | 			ans += mn[i]; | 
27 | 			int x = i; | 
28 | 			while (!bel[x] && x != rt) { | 
29 | 				bel[x] = i, x = fa[x]; | 
30 | 			} | 
31 | 			if (bel[x] == i) { | 
32 | 				++cnt; | 
33 | 				do { | 
34 | 					id[x] = cnt, x = fa[x]; | 
35 | 				} while (!id[x]); | 
36 | 			} | 
37 | 		} | 
38 | 		if (!cnt) { | 
39 | 			break; | 
40 | 		} | 
41 | 		for (int i = 1; i <= n; i++) { | 
42 | 			if (!id[i]) id[i] = ++cnt; | 
43 | 		} | 
44 | 		for (int i = 1; i <= m; i++) { | 
45 | 			int u = e[i].u, v = e[i].v; | 
46 | 			e[i].u = id[u], e[i].v = id[v]; | 
47 | 			if (id[u] != id[v]) e[i].w -= mn[v]; | 
48 | 		} | 
49 | 		rt = id[rt]; | 
50 | 		n = cnt; | 
51 | 	} | 
52 | 	return ans; | 
53 | } | 
54 | |
55 | int main() { | 
56 | 	scanf("%d %d %d", &n, &m, &rt); | 
57 | 	for (int i = 1; i <= m; i++) { | 
58 | 		scanf("%d %d %d", &e[i].u, &e[i].v, &e[i].w); | 
59 | 	} | 
60 | 	printf("%d\n", chu_liu()); | 
61 | 	return 0; | 
62 | } | 
朱 - 刘算法的应用
不确定根的最小树形图
这次我们不规定树形图的根,要求最小树形图。
容易发现我们建立一个超级源点,然后向每一个结点连长度为 $\inf$ 的边,最后算出答案后减去 $\inf$ 即可。发现为了使答案更优,最多只会从超级源点连出一条边,从而保证了去掉超级源点后的根是唯一的。
时间复杂度 $O(nm)$。
「BZOJ 4349」最小树形图
题目大意
有 $n$ 个堡垒,第 $i$ 个堡垒的防御力是 $A_i$,一共要打 $B_i$ 次。有 $m$ 组关系,每组形如 “打了 $x$ 号堡垒以后 $y$ 号堡垒的防御力都会降低为 $z$”,问打完所有堡垒的最小代价。
数据范围:$n \le 50$。
思路分析
容易发现最优策略是所有堡垒打完一遍再慢慢打剩下的,现在问题变成了决定第一次打堡垒的顺序。建立超级源点,向每个堡垒连权值为 $A_i$ 的边。对于每一条关系,从 $x$ 号堡垒向 $y$ 号堡垒连权值为 $z$ 的边即可。根据建图方式容易证明算法正确性。
时间复杂度 $O(nm)$。
代码实现
1 |  | 
2 |  | 
3 | using namespace std; | 
4 | |
5 | typedef double db; | 
6 | const int maxn = 60, maxm = 3e3; | 
7 | const db inf = 1e9 + 1; | 
8 | int n, m, rt, B[maxn + 3], fa[maxn + 3], bel[maxn + 3], id[maxn + 3]; | 
9 | db A[maxn + 3], C[maxn + 3], mn[maxn + 3]; | 
10 | |
11 | struct edge { | 
12 | 	int u, v; | 
13 | 	db w; | 
14 | } e[maxm + 3]; | 
15 | |
16 | db chu_liu() { | 
17 | 	db ans = 0; | 
18 | 	while (true) { | 
19 | 		for (int i = 1; i <= n; i++) { | 
20 | 			mn[i] = inf, fa[i] = 0, bel[i] = 0, id[i] = 0; | 
21 | 		} | 
22 | 		for (int i = 1; i <= m; i++) if (e[i].u != e[i].v) { | 
23 | 			if (e[i].w < mn[e[i].v]) { | 
24 | 				mn[e[i].v] = e[i].w, fa[e[i].v] = e[i].u; | 
25 | 			} | 
26 | 		} | 
27 | 		int cnt = 0; | 
28 | 		for (int i = 1; i <= n; i++) if (i != rt) { | 
29 | 			ans += mn[i]; | 
30 | 			int x = i; | 
31 | 			while (!bel[x] && x != rt) { | 
32 | 				bel[x] = i, x = fa[x]; | 
33 | 			} | 
34 | 			if (bel[x] == i) { | 
35 | 				++cnt; | 
36 | 				do { | 
37 | 					id[x] = cnt, x = fa[x]; | 
38 | 				} while (!id[x]); | 
39 | 			} | 
40 | 		} | 
41 | 		if (!cnt) { | 
42 | 			break; | 
43 | 		} | 
44 | 		for (int i = 1; i <= n; i++) { | 
45 | 			if (!id[i]) id[i] = ++cnt; | 
46 | 		} | 
47 | 		for (int i = 1; i <= m; i++) { | 
48 | 			int u = e[i].u, v = e[i].v; | 
49 | 			e[i].u = id[u], e[i].v = id[v]; | 
50 | 			if (id[u] != id[v]) e[i].w -= mn[v]; | 
51 | 		} | 
52 | 		rt = id[rt]; | 
53 | 		n = cnt; | 
54 | 	} | 
55 | 	return ans; | 
56 | } | 
57 | |
58 | int main() { | 
59 | 	scanf("%d", &n); | 
60 | 	for (int i = 1; i <= n; i++) { | 
61 | 		scanf("%lf %d", &A[i], &B[i]); | 
62 | 		C[i] = A[i]; | 
63 | 	} | 
64 | 	scanf("%d", &m); | 
65 | 	for (int i = 1; i <= m; i++) { | 
66 | 		scanf("%d %d %lf", &e[i].u, &e[i].v, &e[i].w); | 
67 | 		C[e[i].v] = min(C[e[i].v], e[i].w); | 
68 | 	} | 
69 | 	db ans = 0; | 
70 | 	for (int i = 1; i <= n; i++) { | 
71 | 		ans += C[i] * (B[i] - 1); | 
72 | 	} | 
73 | 	for (int i = 1; i <= n; i++) { | 
74 | 		e[++m].u = n + 1, e[m].v = i, e[m].w = A[i]; | 
75 | 	} | 
76 | 	rt = ++n; | 
77 | 	ans += chu_liu(); | 
78 | 	printf("%.2lf\n", ans); | 
79 | 	return 0; | 
80 | } | 
「SCOI 2012」滑雪
题目大意
给定 $n$ 个地点 $m$ 条边,每个地点有高度,每条边有通过时间,且起点高度大于等于终点高度。问这个图以 $1$ 为根的最小树形图。
数据范围:$n \le 10^5, m \le 10^6$。
思路分析
朴素算法不能通过,考虑利用这题的性质。
我们先搜出能够到达的所有点,再将边以终点高度为第一关键字,边权为第二关键字排序后,跑 Kruskal 即可。
时间复杂度 $O(m \log m)$。
代码实现
1 |  | 
2 |  | 
3 | using namespace std; | 
4 | |
5 | typedef long long ll; | 
6 | const int maxn = 1e5, maxm = 2e6; | 
7 | int n, m, k, cnt, h[maxn + 3], tot, ter[maxm + 3], wei[maxm + 3], nxt[maxm + 3], lnk[maxn + 3], fa[maxn + 3]; | 
8 | bool vis[maxn + 3]; | 
9 | |
10 | struct edge { | 
11 | 	int u, v, w; | 
12 | 	friend bool operator < (const edge &a, const edge &b) { | 
13 | 		return h[a.v] == h[b.v] ? a.w < b.w : h[a.v] > h[b.v]; | 
14 | 	} | 
15 | } e[maxm + 3]; | 
16 | |
17 | void add(int u, int v, int w) { | 
18 | 	ter[++tot] = v, wei[tot] = w; | 
19 | 	nxt[tot] = lnk[u], lnk[u] = tot; | 
20 | } | 
21 | |
22 | void dfs(int u) { | 
23 | 	vis[u] = true; | 
24 | 	cnt++; | 
25 | 	for (int i = lnk[u], v, w; i; i = nxt[i]) { | 
26 | 		v = ter[i], w = wei[i]; | 
27 | 		e[++k].u = u, e[k].v = v, e[k].w = w; | 
28 | 		if (!vis[v]) { | 
29 | 			dfs(v); | 
30 | 		} | 
31 | 	} | 
32 | } | 
33 | |
34 | int find(int x) { | 
35 | 	return fa[x] == x ? x : fa[x] = find(fa[x]); | 
36 | } | 
37 | |
38 | int main() { | 
39 | 	scanf("%d %d", &n, &m); | 
40 | 	for (int i = 1; i <= n; i++) { | 
41 | 		scanf("%d", &h[i]); | 
42 | 	} | 
43 | 	for (int i = 1, u, v, w; i <= m; i++) { | 
44 | 		scanf("%d %d %d", &u, &v, &w); | 
45 | 		if (h[u] >= h[v]) add(u, v, w); | 
46 | 		if (h[v] >= h[u]) add(v, u, w); | 
47 | 	} | 
48 | 	dfs(1); | 
49 | 	sort(e + 1, e + k + 1); | 
50 | 	for (int i = 1; i <= n; i++) { | 
51 | 		fa[i] = i; | 
52 | 	} | 
53 | 	ll ans = 0; | 
54 | 	for (int i = 1; i <= k; i++) { | 
55 | 		int u = find(e[i].u), v = find(e[i].v); | 
56 | 		if (u == v) continue; | 
57 | 		fa[u] = v, ans += e[i].w; | 
58 | 	} | 
59 | 	printf("%d %lld\n", cnt, ans); | 
60 | 	return 0; | 
61 | } |