灌水oi旧帖灌水-最小生成树
jessiexichen最小生成树
目录
Kruskal算法
Kruskal算法找到安全边的办法是在所有连接森林中两棵不同树的边里面,找到权重最小的边 ((u,v))。设 (C) 和 (C_2) 为边 ((u,v)) 所连接的两棵树。由于边 ((u,v)) 一定是连接 (C) 和其他某棵树的一条轻量级边,推论隐含告诉我们,边 ((u,v)) 是 (C) 的一条安全边。很显然,Kruskal算法属于贪心算法,因为它每次都选择一条权重最小的边加入到森林。
Kruskal算法的实现与计算连通分量的算法类似。我们使用一个不相交集合数据结构来维护几个互不相交的元素集合。每个集合代表当前森林中的一棵树。操作 FIND-SET(u)
用来返回包含元素 (u) 的集合的代表元素。我们可以通过测试 FIND-SET(u)
是否等于 FIND-SET(v)
来判断节点 (u) 和节点 (v) 是否属于同一棵树。Kruskal算法使用 UNION
过程来对两棵树进行合并。
模板代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| scanf("%d%d", &n, &m); for (int i = 1; i <= m; i++) scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].val);
makeset(n); sort(e + 1, e + m + 1, cmp);
int edgenum = 0; for (int i = 1; i <= m; i++) { if (find(e[i].u) != find(e[i].v)) { Union(e[i].u, e[i].v); MST += e[i].val; edgenum++; if (e[i].val > maxn) maxn = e[i].val; } }
bool cmp(edge x, edge y) { return x.val < y.val; }
int find(int x) { if (fa[x] != x) fa[x] = find(fa[x]); return fa[x]; }
void Union(int x, int y) { int rootx = find(x), rooty = find(y); fa[rootx] = rooty; }
void makeset(int n) { for (int i = 1; i <= n; i++) fa[i] = i; }
|
例题:繁忙的都市
城市 C 是一个非常繁忙的大都市。由于城市中的道路十分拥挤,市长决定对部分道路进行改造。城市 C 的道路布局如下:
- 城市中有多个交叉路口,有些交叉路口之间有道路相连。
- 每两个交叉路口之间最多只有一条双向道路,连接所有交叉路口形成了一个连通图。
- 每条道路都有一个分值,分值越小表示道路越繁忙,且更需要进行改造。
问题描述
由于市政府的资金有限,市长希望在尽量减少改造道路数量的情况下优化交通。市长提出了以下要求:
- 改造的道路能够连通所有交叉路口:即所有交叉路口直接或间接连通。
- 在满足连通要求的前提下,改造的道路数量最少。
- 在满足以上两点的情况下,改造道路的分值中最大分值尽量小。
任务
作为市规划局的一员,你需要作出最佳决策,选择适当的道路进行改造以满足市长的要求。
输入格式
- 第一行包含两个整数 ( n ) 和 ( m ),分别表示城市中的交叉路口数和道路数。
- 接下来的 ( m ) 行,每行包含三个整数 ( u )、( v ) 和 ( c ),表示交叉路口 ( u ) 和 ( v ) 之间有一条分值为 ( c ) 的道路。
约束条件:
- ( 1 \leq n \leq 300 )
- ( 1 \leq c \leq 10000 )
输出格式
- 输出两个整数 ( s ) 和 ( \text{max} ),其中 ( s ) 表示被选中的道路数,( \text{max} ) 表示选中的道路中分值最大的那条道路的分值。
样例输入
1 2 3 4 5 6
| 4 5 1 2 3 1 4 5 2 4 7 2 3 6 3 4 8
|
样例输出
解题思想
此问题可以通过 Kruskal算法 来解决。在构造最小生成树的过程中,我们可以同时记录边的数量和其中最大权值。
Kruskal算法的步骤如下:
- 对所有边按权值从小到大排序,优先选择权值较小的边。
- 使用 并查集 判断当前边的两个节点是否属于同一个连通分量。如果不是,则将此边加入最小生成树,并更新边的数量和最大权值。
- 当所有交叉路口连通后,输出选中的边数和最大权值。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| #include <bits/stdc++.h> using namespace std;
int fa[1600000]; struct edge { int u, v, val; } e[1000000];
int n, m; int MST = 0; int maxn = 0;
bool cmp(edge x, edge y) { return x.val < y.val; }
int find(int x) { if (fa[x] != x) fa[x] = find(fa[x]); return fa[x]; }
void Union(int x, int y) { int rootx = find(x), rooty = find(y); fa[rootx] = rooty; }
void makeset(int n) { for (int i = 1; i <= n; i++) fa[i] = i; }
int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= m; i++) scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].val);
makeset(n); sort(e + 1, e + m + 1, cmp);
int edgenum = 0; for (int i = 1; i <= m; i++) { if (find(e[i].u) != find(e[i].v)) { Union(e[i].u, e[i].v); MST += e[i].val; edgenum++; if (e[i].val > maxn) maxn = e[i].val; } } printf("%d %d", edgenum, maxn); return 0; }
|
PRIM算法
与 Kruskal 算法类似,Prim 算法也是通用最小生成树算法的一个特例。Prim 算法的工作原理与 Dijkstra 的最短路径算法相似。Prim 算法所具有的一个性质是集合 A 中的边总是构成一棵树。这棵树从一个任意的根节点 ( r ) 开始,一直扩展到覆盖图 ( V ) 中的所有节点为止。
算法每一步在连接集合 A 和 A 之外的节点的所有边中,选择一条轻量级边加入到 A 中。根据相关推论,这条规则所加入的边都是对 A 安全的边。因此,当算法终止时,A 中的边形成一棵最小生成树。
这个策略也属于贪心策略,因为每一步都选择当前可行的最优解。
为了有效地实现 Prim 算法,需要一种快速的方法来选择一条新的边,以便加入到由集合 A 中的边所构成的树里。在下面的伪代码中,连通图 ( G ) 和最小生成树的根节点将作为算法的输入。
在算法的执行过程中,所有不在树 A 中的节点都存放在一个基于关键属性的最小优先队列 ( Q ) 中。对于每个节点 ( v ),属性 ( u_k ) 保存的是连接 ( v ) 和树中节点的所有边中最小边的权重。我们约定,如果不存在这样的边,则 ( u_k = \infty )。属性 ( u_{\pi} ) 给出的是节点 ( v ) 在树中的父节点。
Prim 算法将 GENERIC-MST 中的集合 A 维持在以下状态:
[ A = { (o, u_{\pi}) : v \in V \setminus { r } } ]
当 Prim 算法终止时,最小优先队列 ( Q ) 将为空,而 ( G ) 的最小生成树 ( A ) 则是:
[ A = { (o, u_{\pi}) : v \in V \setminus { r } } ]
Prim 算法实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| void prim(int start) { for (int i = 1; i <= n; i++) parent[i] = -1; dis[start] = 0; visit[start] = 0;
for (int i = 1; i <= n; i++) { int u = -1, DIST = INT_MAX; for (int j = 1; j <= n; j++) { if (visit[j] == 0 && DIST > dis[j]) { DIST = dis[j]; u = j; } } MST += DIST; visit[u] = 1;
for (int j = 1; j <= n; j++) { if (visit[j] == 0 && mat[u][j] < dis[j]) { dis[j] = mat[u][j]; parent[j] = u; } } } }
|
例题:最优布线问题
学校有台计算机,为了方便数据传输,现要将它们用数据线连接起来。两台计算机被连接是指它们有数据线连接。由于计算机所处的位置不同,因此不同的两台计算机的连接费用往往是不同的。
当然,如果将任意两台计算机都用数据线连接,费用将是相当庞大的。为了节省费用,我们采用数据的间接传输手段,即一台计算机可以间接地通过若干台计算机(作为中转)来实现与另一台计算机的连接。
现在由你负责连接这些计算机,任务是使任意两台计算机都连通(不管是直接的或间接的)。
以下是关于最优布线问题的输入输出格式整理,使用 Markdown 语言:
输入
第一行为整数 ( n ) ( ( 2 \leq n \leq 100 ) ),表示计算机的数目。此后的 ( n ) 行,每行 ( n ) 个整数。第 ( x+1 ) 行 ( y ) 列的整数表示直接连接第 ( x ) 台计算机和第 ( y ) 台计算机的费用。
输出
一个整数,表示最小的连接费用。
样例输入
3
0 1 2
1 0 1
2 1 0
样例输出
2
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| #include <bits/stdc++.h> using namespace std;
int mat[105][105], MST, parent[1005], dis[1005];
int visit[1005], n; const int oo = INT_MAX;
void prim(int start) { for (int i = 1; i <= n; i++) { parent[i] = -1; dis[i] = oo; visit[i] = 0; } dis[start] = 0;
for (int i = 1; i <= n; i++) { int u = -1, DIST = oo; for (int j = 1; j <= n; j++) { if (visit[j] == 0 && DIST > dis[j]) { DIST = dis[j]; u = j; } } MST += DIST; visit[u] = 1;
for (int j = 1; j <= n; j++) { if (visit[j] == 0 && mat[u][j] < dis[j]) { dis[j] = mat[u][j]; parent[j] = u; } } } }
int main() { scanf("%d", &n); for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) scanf("%d", &mat[i][j]); prim(1); printf("%d", MST); return 0; }
|