1、定义:
在连通无向图G(V,E)
中找到一个边E
的无环子集T
,使其能够将所有节点连接起来,又具有最小权重。
a)由于T是无环的,可看作是一棵树;
b)由于是图G生成的,所以称为(图G的)生成树;
c)由于T具有权重最小,所以称为最小生成树。
由于T是无环的,所以V个顶点一定有V-1条边。
2、贪心策略:
每次生长最小生成树的一条边,进行V-1次循环完成建树。生长的边必须轻量级边(权重最小)且不构成环。
方法: kruskal(克鲁斯卡尔)算法或prim(普里姆)算法
3、prim(普里姆)算法:
此算法可以称为加点法,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从任一个顶点s开始,逐渐长大覆盖整个连通图的所有顶点。
3.1、算法流程及图:
输入:图G(V,E)
与任一节点u
作为起始点;输出:边集合P
- 节点
u
加入节点集合U
; - 选择与集合
U
中节点相连的代价最小边加入集合P
(如边<u,v>
),相连节点(如v
)加入集合U
;
(如果存在有多条边具有相同权值,则可任意选取其中之一); - 重复进行步骤2,直至所有节点加入集合
U
。
3.2、C++代码:
#include <queue>
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct edge { // 边的定义
int start;
int end;
int weight;
edge(int x, int y, int z) :start(x), end(y), weight(z) {}
bool operator< (const edge& tmp) const { //用于优先队列的比较函数
return weight > tmp.weight;
}
};
const int NUM = 6;
int G[NUM][NUM]; //假设图已经定义好了,如G[0][2] = 5; 5为权重
void MiniSpanTree_prim(int(*G)[NUM], int root)
{
vector<int> V; //记录MST已经包含的点;
V.push_back(root);
priority_queue<edge> edge_all; // 放入权重边,自动排序
for (int i = 0; i < NUM; i++) { // 放入与起点连接的边
if (G[root][i] != 0) { // 0 代表没有路
edge tmp(root, i, G[root][i]);
edge_all.push(tmp);
}
}
cout << "Prim :" << endl;
for (int i = 0; i < NUM - 1; i++) { //共N-1条边
edge curr = edge_all.top(); //取得代价最小边
edge_all.pop();
while (find(V.begin(), V.end(), curr.end) != V.end()) { //边终点若已包含,则丢弃后换一条边
curr = edge_all.top();
edge_all.pop();
}
V.push_back(curr.end); //放入这条边的终点v
cout << curr.start << " --> " << curr.end << " " << curr.weight << endl; // 输出MST的边
for (int j = 0; j < NUM; j++) { // 加入终点v连接的边
if (G[curr.end][j] != 0 && find(V.begin(), V.end(), j) == V.end()) {
edge tmp(curr.end, j, G[curr.end][j]);
edge_all.push(tmp);
}
}
}
}
4、kruskal(克鲁斯卡尔)算法:
此算法可以称为加边法,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
4.1、算法流程及图:
输入:图G(V,E)
;输出:边集合P。
- 把图中的所有边按代价从小到大排序;
- 把图中的n个顶点看成独立的n棵树组成的森林;
- 按权值从小到大选择边,所选边的两个顶点
u,v
若属于两颗不同的树,则成为最小生成树的一条边,同时将这两颗树合并作为一颗树。否则取下一条权值最小的边再试。 - 重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。
4.2、C++代码:
#include <queue>
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct edge { // 边的定义
int start;
int end;
int weight;
edge(int x, int y, int z) :start(x), end(y), weight(z) {}
bool operator< (const edge& tmp) const { //用于优先队列的比较函数
return weight > tmp.weight;
}
};
const int NUM = 6;
int G[NUM][NUM]; //假设图已经定义好了,如G[0][2] = 5; 5为权重
void MiniSpanTree_kruskal(int(*G)[NUM])
{
int node_coll[NUM] = { 0 }; //判断节点是否属于一棵树
int count = 1; // 用于不同树的标记
priority_queue<edge> edge_all; // 放入权重边,自动排序
for (int i = 0; i<NUM; i++) { //放入所有边,并自动排序
for (int j = 0; j<NUM; j++) {
if (G[i][j] != 0) {
edge tmp(i, j, G[i][j]);
edge_all.push(tmp);
}
}
}
for (int i = 0; i<NUM - 1; i++) { //共N-1条边
edge tmp = edge_all.top(); // 取代价最小边
edge_all.pop();
while (node_coll[tmp.start] == node_coll[tmp.end] && node_coll[tmp.start] != 0 && node_coll[tmp.end] != 0) { // 找到属于不同树的边
tmp = edge_all.top();
edge_all.pop();
}
cout << tmp.start << " --> " << tmp.end << " " << tmp.weight << endl; // 输出MST的边
if (node_coll[tmp.start] == 0 && node_coll[tmp.end] == 0) { //不同树的边的情况1
node_coll[tmp.start] = count;
node_coll[tmp.end] = count;
count++;
}
else if (node_coll[tmp.start] == 0 && node_coll[tmp.end] != 0) { //不同树的边的情况2
node_coll[tmp.start] = count;
node_coll[tmp.start] = node_coll[tmp.end];
}
else if (node_coll[tmp.start] != 0 && node_coll[tmp.end] == 0) { //不同树的边的情况3
node_coll[tmp.start] = count;
node_coll[tmp.end] = node_coll[tmp.start];
}
else if (node_coll[tmp.start] != 0 && node_coll[tmp.end] != 0) { //不同树的边的情况4
node_coll[tmp.start] = count;
for (int i = 0; i<NUM; i++) {
if (node_coll[i] = node_coll[tmp.end]) {
node_coll[i] = node_coll[tmp.start];
}
}
}
}
}
参考资料:最小生成树的两种方法(Kruskal算法和Prim算法)
算法导论 23.2 Kruskal算法和Prim算法
总结:
1、kruskal(克鲁斯卡尔)算法为加边法,prim算法为加点法,记忆算法思路就好。