分类-第三章 搜索与图论-最小生成树
题目
给定一个 n n n 个点 m m m 条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。
给定一张边带权的无向图 G = ( V , E ) G=(V,E) G=(V,E),其中 V V V 表示图中点的集合, E E E 表示图中边的集合, n = ∣ V ∣ n=|V| n=∣V∣, m = ∣ E ∣ m=|E| m=∣E∣。
由 𝑉 中的全部 n n n个顶点和 E E E 中 n − 1 n-1 n−1 条边构成的无向连通子图被称为 G G G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G G G 的最小生成树。
输入格式
第一行包含两个整数 n n n 和 m m m 。
接下来 m m m 行,每行包含三个整数 u , v , w u,v,w u,v,w,表示点 u u u 和点 v v v 之间存在一条权值为 w w w 的边。
输出格式
共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。
数据范围
1
≤
n
≤
500
,
1≤n ≤500,
1≤n≤500,
1
≤
m
≤
1
0
5
,
1≤m≤10^5,
1≤m≤105,
图中涉及边的边权的绝对值均不超过 10000。
输入样例
4 5
1 2 1
1 3 2
1 4 3
2 3 2
3 4 4
输出样例
6
解题前的"废话"
什么是最小生成树?
一个有 n n n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n n n个结点,并且有保持图连通的最少的边。 [1]最小生成树可以用kruskal(克鲁斯卡尔)算法或prim(普里姆)算法求出。(来自百度百科)
极小连通子图中边权和最小的就是最小生成树,当然如果边权和出现相同的情况,就说明最小生成树不唯一,所以一般结果是求值。
什么是Prim算法?—适合边多点少
从个人理解来说, P r i m Prim Prim就是一个不断找最小边的算法(先找边,再确定点)。首先分为两个点集, s t st st代表被选中的点集,另外一个点集代表未被选中。开始前st为空,选择从任意一点出发,并加入 s t st st。选择与所选点集相连最小的边,每次确定边之前,保证之前所选边不会成环。重复选边、判断的步骤,直到所有点被选中。
思路(结合代码一起看)
大体思路是,我们要将我们的手算模拟思路转化为代码,再加上代码优化,更简洁美观(为了偷懒)。
先定义三个数组:
s t [ N ] 、 d i s t [ N ] 、 g [ N ] [ N ] , st[N]、dist[N]、g[N][N], st[N]、dist[N]、g[N][N],
分别代表已选点集、点集中任意一点到点 Vi 的距离,以及任意两点之间的距离。
(1)一共有 n n n个点,我们需要循环遍历$ n $次,直到每个点都被选中。
(2)开始时任意选一个点。假设第一次选V1 ,加入st集合(其实第一次进入双重for循环其实什么都没干)。更新每个点到V1的最短距离,然后进入下一次循环。
(3)(正式开始)每次选未被选择过的点Vj,即st[j]=false,且Vj到集合st中的某一个点是所有边种最短的(第二重for循环中有体现);如果没有最短的边(都是INF),那么就说明不存在最小生成树,返回无穷大INF。
(4)有两个点了 (i不为0了),最小生成树中就存在一条边了,加入存储答案的res。
(5)然后重复步骤(2),将选中的点 Vt 加入点集
s
t
st
st…
示例
选择从任意一点出发,此处选择V1。
更新所有到已选的点集合(黄色所圈的点)的最短距离,找出最短的边(V1,V0)
将点V0加入点集
更新边,选择最短的边(V0,V5)
选择最短(V1,V8) (dist[6]=6,记录的是最短的边)
中间步骤一样跳过,来看下一下特殊的情况(dist[3]=11): 最短边是(V5,V6),但V5,V6已经是st 集合中的点了,所以不能加入(V1,V2)这条边也是,所以剔除这两条边,最终第二重循环选出来的t=7,也就是V7。
直到所有点被选中,得到的结果就是最小生成树。当然在选的过程中,最短边如果有两条,且符合选择条件,则最小生成树是不唯一的。
代码
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=510,M=1e5, INF=0x3f3f3f3f;
int n,m;
int g[N][N];//邻接矩阵存储权
bool st[N];//存储在集合中的点
int dist[N];//存储某点到集合的距离
int prim()
{
//
memset(dist,0x3f,sizeof dist);
int res=0;//用来存储最小生成树边的和
for(int i=0;i<n;i++) //循环n次,每次找到一个点加入集合
{
int t=-1;
//这里是寻找离集合最短的边
for(int j=1;j<=n;j++)//从节点1~n
if(!st[j]&&(t==-1||dist[j]<dist[t]))
t=j;//更新成较短的
//找到最短的边之后,先更新最小树边之和,而不是更新边
//如果有自环,且自环路最短,则自环边会被更新为短边然后算入最小生成树
//先加入点的集合,则不会考虑自环边
if(i&&dist[t]==INF) return INF;//如果找不到更新点了则说明无
if(i) res+=dist[t];
st[t]=true;//t被放入最小树点集合
//更新集合中刚加入的点与节点j连接的边
for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
}
return res;
}
int main()
{
cin>>n>>m;
//初始化图的距离,无边代表无穷远
memset(g,0x3f,sizeof g);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
g[a][b]=g[b][a]=min(g[a][b],c);
}
int t=prim();
if(t==INF) puts("impossible");
else cout<<t<<endl;
return 0;
}
(算法小白,写博客为了对算法理解更透彻。如有错误,望大佬指点,欢迎交流!)