什么是最小树形图
无向图上有最小生成树,那么有向图上呢?有向图上的最小生成树称为 “最小树形图”,英文叫 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 | } |