最小生成树
一、定义
图的所有生成树中具有边上的权值之和最小的树称为图的最小生成树 (Minimum Spanning Tree,MST) ;
一个连通图的生成树是一个极小连通子图,它含有图中全部顶点,但只有构成一棵树的 n − 1 n-1 n−1 条边;
对于一个带权 (假定每条边上的权均为大于零的数) 连通无向图 G G G 中的不同生成树,其每棵树的所有边上的权值之和也可能不同;
简单来说,对于一个有 n n n 个点的图,边一定是大于等于 n − 1 n-1 n−1 条的,最小生成树就是在这些边中选择 n − 1 n-1 n−1 条出来连接所有的 n n n 个点,且这 n − 1 n-1 n−1 条边的边权之和是所有方案中最小的;
二、Prim 算法
1. 思路
Prim 算法是一种构造性算法;
假设 G = ( V , E ) G = (V, E) G=(V,E) 是一个具有 n n n 个顶点的带权连通无向图, T = ( U , T E ) T = (U, TE) T=(U,TE) 是 G G G 的最小生成树,其中 U U U 是 T T T 的顶点集, T E TE TE 是 T T T 的边集,则由 G G G 构造从起始顶点 v v v 出发的最小生成树 T T T 的步骤如下:
-
初始化 U = { v } U = \{v\} U={v} ;以 v v v 到其他顶点的所有边为候选边;
-
重复以下步骤 n − 1 n-1 n−1 次,使得其他 n − 1 n-1 n−1 个顶点被加入到 U U U 中:
-
以顶点集 U U U 和顶点集 V − U V - U V−U 之间的所有边(称为割集 ( U , V − U ) (U, V - U) (U,V−U) )作为候选边,从中挑选权值最小的边(称为轻边)加入 T E TE TE ,设该边在 V − U V - U V−U 中的顶点是 k k k ,将 k k k 加入 U U U 中;
-
考察当前 V − U V - U V−U 中的所有顶点 j j j ,修改候选边,若 ( k , j ) (k,j) (k,j) 的权值小于原来和顶点 j j j 关联的候选边,则用 ( k , j ) (k, j) (k,j) 取代后者作为候选边;
2. 例子
使用 Prim 算法生成图的最小生成树,以结点 6 作为起点;
将结点划分成两个集合 U U U 和 V − U V-U V−U ;
-
U = { 6 } , V − U = { 1 , 2 , 3 , 4 , 5 , 7 } U=\{6\}, V-U=\{1, 2, 3, 4, 5, 7\} U={6},V−U={1,2,3,4,5,7} ;
候选边有 (6, 1, 1) 和 (6, 5, 8) ;
选择 (6, 1, 1) 为最小生成树的一条边,并且将结点1加入到 U U U 集合中;
此时最小生成树为,
-
U = { 6 , 1 } , V − U = { 2 , 3 , 4 , 5 , 7 } U = \{ 6, 1 \}, V - U = \{ 2, 3, 4, 5, 7 \} U={6,1},V−U={2,3,4,5,7} ;
侯选边有 (1, 2, 6) 和 (6, 5 ,8) ;
选择 (1, 2, 6) 为最小生成树的一条边,并且将结点2加入 U U U 集合中;
此时最小生成树为,
- U = { 6 , 1 , 2 , 7 } , V − U = { 3 , 4 , 5 } U = \{ 6, 1, 2, 7 \}, V - U = \{ 3, 4, 5 \} U={6,1,2,7},V−U={3,4,5} ;
侯选边有 (6, 5, 8), (7, 5, 7), (7, 4, 5), (2, 3, 4);
选择 (2, 3, 4) 为最小生成树的一条边,并且将结点3加入 U U U 集合中;
此时最小生成树为,
- U = { 6 , 1 , 2 , 7 , 3 } , V − U = { 4 , 5 } U = \{ 6, 1, 2, 7, 3 \}, V - U = \{ 4, 5 \} U={6,1,2,7,3},V−U={4,5} ;
侯选边有 (6, 5, 8), (7, 5, 7), (4, 5, 6) ;
选择 (4, 5, 6) 为最小生成树的一条边,并且将结点5加入 U U U 集合中;
n − 1 n - 1 n−1 轮执行完毕,得到最小生成树;
3. 实现
以1为起点生成最小生成树, d i s [ v ] dis[v] dis[v] 表示与 v v v 相连的最小边权, M S T MST MST 表示最小生成树的权值之和;
-
初始化, d i s v = ∞ dis_v = \infty disv=∞ ,
dis[1] = 0, MST = 0
; -
遍历 1 ∼ n 1 \sim n 1∼n 号节点,
寻找 d i s [ x ] dis[x] dis[x] 最小的未进入最小生成树的节点 x x x ;
将 x x x 标记为已进入最小生成树;
M S T MST MST 加上当前边;
枚举与 x x x 相邻的未进入最小生成树的节点 v v v ;
若 ( x , v ) (x, v) (x,v) 的权值小于原来和顶点 v v v 关联的候选边,则用 ( x , v ) (x, v) (x,v) 取代后者作为候选边;
-
结束遍历, M S T MST MST 即为最小生成树的权值之和;
4. 代码
int n, m, dis[MAXN], MST;
bool vis[MAXN];
struct edge {
int to, tot;
};
vector <edge> g[MAXN];
void Prim() {
memset(vis, false, sizeof(vis));
memset(dis, 0x3f, sizeof(dis));
dis[1] = 0;
for (int i = 1; i <= n; i++) {
int x, minn = 2147483647;
for (int j = 1; j <= n; j++) {
if (!vis[j] && dis[j] < minn) { // 在所有未加入最小生成树的结点中找到距离更近的结点
minn = dis[j];
x = j;
}
}
vis[x] = true;
for (int j = 0; j < g[x].size(); j++) {
int v = g[x][j].to, tot = g[x][j].tot;
if (!vis[v] && dis[v] > tot) { // 利用新加入的结点k对还未加入到的最小生成树的结点距离进行松弛操作
dis[v] = tot;
}
}
MST += dis[x];
}
return;
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; i++) {
int x, y, z;
scanf("%d %d %d", &x, &y, &z);
g[x].push_back(edge({y, z}));
g[y].push_back(edge({x, z}));
}
Prim();
printf("%d", MST);
return 0;
}
5. 堆优化
寻找所有未加入最小生成树的结点中距离最近的结点时,可使用优先队列优化;
int n, m, dis[MAXN], MST; // dis[i]表示与i相连的最短路径
bool vis[MAXN]; // vis[i] 为i是否加入最小生成树
struct edge {
int to, tot; // to为终点,tot为边权
bool operator < (const edge &a) const {
return tot > a.tot; // 按照边权从小到大排序
}
};
vector <edge> g[MAXN];
void Prim() {
memset(dis, 0x3f, sizeof(dis));
memset(vis, false, sizeof(vis));
dis[1] = 0; // 初始化
priority_queue <edge> q;
q.push(edge({1, 0}));
while (!q.empty()) {
int x = q.top().to;
q.pop();
if (vis[x]) continue;
vis[x] = true;
for (int i = 0; i < g[x].size(); i++) {
int v = g[x][i].to, tot = g[x][i].tot;
if (!vis[v] && dis[v] > tot) {
dis[v] = tot; // 将距离最近的结点加入到最小生成树中
q.push(edge({v, dis[v]}));
}
}
MST += dis[x];
}
return;
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; i++) {
int x, y, z;
scanf("%d %d %d", &x, &y, &z);
g[x].push_back(edge({y, z}));
g[y].push_back(edge({x, z}));
}
Prim();
printf("%d", MST);
return 0;
}
三、Kruskal 算法
1. 思路
Kruskal 算法是一种按权值的递增次序选择合适的边来构造最小生成树的方法;
假设 G = ( V , E ) G=(V, E) G=(V,E) 是一个具有 n n n 个顶点 e e e 条边的带权连通无向图, T = ( U , T E ) T=(U,TE) T=(U,TE) 是 G G G 的最小生成树,则构造最小生成树的步骤如下;
- 置 U U U 的初值等于 v v v (即包含有 G G G 中的全部顶点), T E TE TE 的初值为空集(即图 T T T 中每一个顶点都构成一个分量);
- 将图 G G G 中的边按权值从小到大的顺序依次选取:若选取的边未使生成树 T T T 形成回路,则加入 T E TE TE ;否则舍弃,直到 T E TE TE 中包含 n − 1 n-1 n−1 条边为止;
实现 Kruskal 算法的关键是如何判断选取的边是否与生成树中己有的边形成回路,这可以通过并查集来解决;
2. 例子
首先对图中的边按照边权从小到大排序;
按照边权从小到大的顺序依次将每条边加入到最小生成树中,但不能产生回路;
依次加入 (1, 6, 1), (3, 4, 2), (2, 7, 3), (2, 3, 4) ;
当加入边 (4, 7, 5) 时,发现会产生回路,所以跳过此边;
再按照相同的方法依次加入 (1, 2, 6), (4, 5, 6) ;
当加入边 (5, 7, 7) 时,发现会产生回路,所以跳过此边;
当加入边 (5, 6, 8) 时,发现会产生回路,所以跳过此边;
至此所有结点己经加入到了最小生成树中,算法结束;
3. 实现
-
初始化
每个点的父节点初始化为自己,
MST = 0
; -
遍历每一条边
若边的两端点不在同一集合,合并两点, M S T MST MST 加上当前边;
-
结束遍历, M S T MST MST 即为最小生成树的权值之和。
4. 代码
int n, m, father[MAXN], MST;
struct edge {
int x, y, z; // (x, y) 边,边权为 z
bool operator < (const edge a) { // 边权从小到大排序
return z < a.z;
}
} g[EDGE_MAXN];
void firstset(int n) { // 初始化并查集
for (int i = 1; i <= n; i++) {
father[i] = i;
}
return;
}
int findset(int x) { // 查找并查集初始点
if (father[x] == x) return x;
else return father[x] = findset(father[x]);
}
void Kruskal(int n, int m) {
firstset(n);
sort(1 + g, 1 + g + m); // 边按边权排序
for (int i = 1; i <= m; i++) { // 从小到大便利每一条边
int x = findset(g[i].x), y = findset(g[i].y);
if (x != y) { // 边的两端点不在同一集合
father[x] = y; // 合并
MST += g[i].z; // 最小生成树加上当前边
}
}
return;
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= m * 2; i += 2) {
int x, y, z;
scanf("%d %d %d", &x, &y, &z);
g[i].x = g[i + 1].x = x;
g[i].y = g[i + 1].y = y;
g[i].z = g[i + 1].z = z;
}
Kruskal(n, m * 2);
printf("%d\n", MST);
return 0;
}