只要你跑的够快,锅就追不上你

0%

「学习笔记」朱 - 刘算法求解最小树形图

什么是最小树形图

无向图上有最小生成树,那么有向图上呢?有向图上的最小生成树称为 “最小树形图”,英文叫 Directed Minimum Spanning Tree。

如果把一个树形图的有向边替换成无向边,它会变成一棵生成树。但与生成树不同的是,树形图中会确定一个根,它必须满足根能够到达每个结点。最小树形图是所有树形图中边权和最小的一个。

朱 - 刘算法简介

一般来讲在图中求最小树形图时会确定一个根结点。有一个时间复杂度为 $O(nm)$ 的算法可以解决这个问题:朱 - 刘算法(英文 Chu - Liu Algorithm),是两个中国人一起发明的算法。

朱 - 刘算法的基本思想是每次贪心地选出每个点最小的父亲,得出的图如果不是树形图,那么将选出的环进行缩点,对边权进行修改,然后迭代这个过程,直到图变为最小树形图为止。

朱 - 刘算法模版

建议看一下「模板」最小树形图(Luogu 4716) 这题的题解,里面有确定根结点的最小树形图详细的讲解。注意大部分题解的时间复杂度都是 $O(n^3)$ 而不是 $O(nm)$ 的,问题出在它们找环的过程不能保证复杂度。如果不会找环的话可以看看「NOIP 2015」信息传递(Luogu 2661)这题,题解中的复杂度都是正确的。

1
#include <cstdio>
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」最小树形图

题目大意

「BZOJ 4349」最小树形图

有 $n$ 个堡垒,第 $i$ 个堡垒的防御力是 $A_i$,一共要打 $B_i$ 次。有 $m$ 组关系,每组形如 “打了 $x$ 号堡垒以后 $y$ 号堡垒的防御力都会降低为 $z$”,问打完所有堡垒的最小代价。

数据范围:$n \le 50$。

思路分析

容易发现最优策略是所有堡垒打完一遍再慢慢打剩下的,现在问题变成了决定第一次打堡垒的顺序。建立超级源点,向每个堡垒连权值为 $A_i$ 的边。对于每一条关系,从 $x$ 号堡垒向 $y$ 号堡垒连权值为 $z$ 的边即可。根据建图方式容易证明算法正确性。

时间复杂度 $O(nm)$。

代码实现

1
#include <cstdio>
2
#include <algorithm>
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」滑雪

题目大意

「SCOI 2012」滑雪(Luogu 2573)

给定 $n$ 个地点 $m$ 条边,每个地点有高度,每条边有通过时间,且起点高度大于等于终点高度。问这个图以 $1$ 为根的最小树形图。

数据范围:$n \le 10^5, m \le 10^6$。

思路分析

朴素算法不能通过,考虑利用这题的性质。

我们先搜出能够到达的所有点,再将边以终点高度为第一关键字,边权为第二关键字排序后,跑 Kruskal 即可。

时间复杂度 $O(m \log m)$。

代码实现

1
#include <cstdio>
2
#include <algorithm>
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
}