图论 - 无向图最小生成树
N个点M条边的无向连通图,每条边有一个权值,求该图的最小生成树。
Input
第1行:2个数N,M中间用空格分隔,N为点的数量,M为边的数量。(2 <= N <= 1000, 1 <= M <= 50000)
第2 - M + 1行:每行3个数S E W,分别表示M条边的2个顶点及权值。(1 <= S, E <= N,1 <= W <= 10000)
Output
输出最小生成树的所有边的权值之和。
Input示例
9 14
1 2 4
2 3 8
3 4 7
4 5 9
5 6 10
6 7 2
7 8 1
8 9 7
2 8 11
3 9 2
7 9 6
3 6 4
4 6 14
1 8 8
Output示例
37
---------------------------------------------------------------我是分割线^_^-----------------------------------------------------------
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
const int INF = 0x3f3f3f3f;
const int MAXN = 1005;
const int MAXM = 50005;
int cost[MAXN][MAXN];
struct Edge
{
int s, t, d; // s为起点,t为终点,d为权值
} edge[MAXM];
/**
* prim算法
* @param cost: 赋权邻接矩阵
* @param n: 节点的编号,为1-n
*/
int prim(int cost[][MAXN], int n)
{
int rst = 0;
bool vis[MAXN]; // 标记节点是否被加入
int label[MAXN]; // 选择节点的依据
memset(vis, false, sizeof(vis));
vis[1] = true; // 从任意一个节点开始
for (int i = 1; i <= n; ++i) {
label[i] = cost[1][i];
}
for (int times = 1; times <= n - 1; ++times) { // 循环n - 1次,每次加入一个节点
int minEdge = INF;
int choosed = -1;
for (int i = 1; i <= n; ++i) { //选择可扩张边上的节点
if (!vis[i] && label[i] < minEdge) {
minEdge = label[i];
choosed = i;
}
}
if (minEdge == INF) { // mincost没更新,说明原图没有联通
return -1;
}
rst += minEdge;
vis[choosed] = true;
for (int i = 1; i <= n; ++i) { // 更新节点的标记值
if (!vis[i] && cost[choosed][i] < label[i]) {
label[i] = cost[choosed][i];
}
}
}
return rst;
}
void addEdge(int id, int s, int t, int d)
{
edge[id].s = s;
edge[id].t = t;
edge[id].d = d;
}
bool cmp(Edge edge1, Edge edge2)
{
return edge1.d < edge2.d;
}
int unionFind(int pre[], int x) {
int r = x;
while (pre[r] != r) {
r = pre[r];
}
return r;
}
/**
* Kruskal算法
* @param
*/
int kruskal(Edge edge[], int n, int m)
{
int rst = 0;
int pre[MAXN]; // 并查集
for (int i = 1; i <= n; ++i) {
pre[i] = i;
}
sort(edge, edge + m, cmp); // 对所有边按权值从小到大排序
int cnt = 0; // 对加入的边计数
for (int i = 0; i < m; ++i) {
int r1 = unionFind(pre, edge[i].s);
int r2 = unionFind(pre, edge[i].t);
if (r1 != r2) { //不构成环, 就加入生成树
pre[r1] = r2;
rst += edge[i].d;
cnt++;
}
if (cnt >= n - 1) { // 已经加入n - 1条边,可以结束了
break;
}
}
if (cnt < n - 1) { // 加入的边小于n - 1,说明原图不联通
return -1;
}
return rst;
}
int main()
{
int n, m;
int s, t, d;
scanf("%d%d", &n, &m);
if (m >= n * (n - 1) / 4) { // ?????Kruskal
for (int i = 0; i < m; ++i) {
scanf("%d%d%d", &s, &t, &d);
addEdge(i, s, t, d);
}
cout << kruskal(edge, n, m) << endl;
} else { // ?????Prim
memset(cost, INF, sizeof(cost));
for (int i = 0; i < m; ++i) {
scanf("%d%d%d", &s, &t, &d);
cost[s][t] = min(cost[s][t], d);
cost[t][s] = cost[s][t];
}
cout << prim(cost, n) << endl;
}
return 0;
}
给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树.
一个连通图的生成树是一个极小连通子图,它含有图中全部顶点,但只有足以构成一棵树的n-1条边。
求最小生成树的算法
(1) 克鲁斯卡尔算法
图的存贮结构采用边集数组,且权值相等的边在数组中排列次序可以是任意的.该方法对于边相对比较多的不是很实用,浪费时间.
(2) 普里姆算法
图的存贮结构采用邻接矩阵.此方法是按各个顶点连通的步骤进行,需要用一个顶点集合,开始为空集,以后将以连通的顶点陆续加入到集合中,全部顶点加入集合后就得到所需的最小生成树 .
下面来具体讲下:
克鲁斯卡尔算法
方法:将图中边按其权值由小到大的次序顺序选取,若选边后不形成回路,则保留作为一条边,若形成回路则除去.依次选够(n-1)条边,即得最小生成树.(n为顶点数)
第一步:由边集数组选第一条边
第二步:选第二条边,即权值为2的边
第三步:选第三条边,即权值为3的边
第四步:选第四条边,即权值为4的边
第五步:选第五条边
普里姆算法
方法:从指定顶点开始将它加入集合中,然后将集合内的顶点与集合外的顶点所构成的所有边中选取权值最小的一条边作为生成树的边,并将集合外的那个顶点加入到集合中,表示该顶点已连通.再用集合内的顶点与集合外的顶点构成的边中找最小的边,并将相应的顶点加入集合中,如此下去直到全部顶点都加入到集合中,即得最小生成树.
例在下图中从1点出发求出此图的最小生成树,并按生成树的边的顺序将顶点与权值填入表中.
———————>先写出其邻接矩阵
第一步:从①开始,①进集合,用与集合外所有顶点能构成的边中找最小权值的一条边
①——②权6
①——③权1 -> 取①——③边
①——④权5
第二步:③进集合,①,③与②,④,⑤,⑥构成的最小边为
①——④权5
③——⑥权4 -> 取③——⑥边
第三步:⑥进集合,①,③,⑥与②,④,⑤构成的各最小边
①——②权6
③——②权5
⑥——④权2 -> 取⑥——④边
第四步:④进集合,①,③,⑥,④与②,⑤构成的各最小边
①——②权6
③——②权5 -> 取③——②边
⑥——⑤权6
第四步:②进集合,①,③,⑥,②,④与⑤构成的各最小边
②——⑤权3 -> 取②——⑤边
这也是在网上找到的一个Kruskal和Prim构造过程图,贴出来: