什么是最小生成树
一个有N个点的图,边一定是大于等于N-1条的。图的最小生成树,就是在这些边中选择N-1条出来,连接所有的N个点。这N-1条边的边权之和是所有方案中最小的。
向上图中的最小生成树如下:
将一个图中的所有节点用最少得边连接起来并且这些边的值加起来最小,这就是最小生成树了
解决最小生成树一般有两种算法:prim和kruskal算法,接下来我分别讲解一下
Prim
prim算法是以点为核心,就是判断当前树外,哪些点距离当前树的距离最小,我就把它加入到我的最小生成树中 ,例如:假设当前我以1节点为我的最小生成树,因为与这个树相邻的节点只有2和3节点,这时我就我会选择1->2 和1->3中边权值最小的边加入到这个生成树中,当前这俩一样,所以我就将2节点加入到这个生成树中
然后再判断与当前生成树相邻的节点中,哪个边权值最低,再将对应相连的节点加入到生成树中,依次类推,知道所有节点都加入到这个生成树中,这就是最小生成树了
代码
#include<iostream>
#include<vector>
#include<climits>
using namespace std;
int main(){
int v,e;// 一共有v个点和e条边
int x,y,k;
cin>>v>>e;
vector<vector<int>> grid(v+1,vector<int>(v+1,10001));
while(e--){//将这些边用邻接矩阵储存起来
cin>>x>>y>>k;
grid[x][y]=k;
grid[y][x]=k;
}
vector<int> minDist(v+1,10001);//这个数组记录每个节点距离当前最小生成树的距离,默认为最大值
vector<bool> isInTree(v+1,false);//这个数组记录哪些节点最小生成树中,默认都不在
for(int i=1;i<v;i++){//遍历v-1个节点
int cur=-1;//记录距离当前树最近距离的节点
int minval=INT_MAX;//默认距离为最大值
for(int j=1;j<=v;j++){//遍历每个节点距离当前树,记录最短距离的节点
if (!isInTree[j] && minval>minDist[j]){
minval=minDist[j];
cur=j;
}
}
//这时cur就是距离当前树的最近点
isInTree[cur]=true;
for(int j=1;j<=v;j++){//以cur节点为基础,更新与cur节点相连节点距离当前树的距离
if(!isInTree[j]&&grid[cur][j]<minDist[j]){
minDist[j]=grid[cur][j];
}
}
}
int result=0;//记录当前最小生成树的边权值的和
for(int i=2;i<=v;i++){
result+=minDist[i];
}
cout<<result<<endl;
}
kruskal:
kruskal算法思想就是记录所有的边,将所有的边从小到大排序一下,每次贪心的用边权值最小的边连接两个节点,如果遍历到当前边的时候发现这条边对应的两个节点已经链接到一起了,就跳过这条边,继续遍历
这个思路非常简单,但是代码就不好实现,比如如何判断两个节点是否链接到一起了,这里就用到了并查集,没有学过并查集的小伙伴先去学完并查集再来学习本算法哦~
首先我们先定义边,以及将所有的边进行排序:
// l,r为 边两边的节点,val为边的数值
struct Edge {
int l, r, val;
};
int main() {
int v, e;//记录有v个节点和e条边
int v1, v2, val;
vector<Edge> edges;
int result_val = 0;
cin >> v >> e;
while (e--) {//用邻接表的方式记录图
cin >> v1 >> v2 >> val;
edges.push_back({v1, v2, val});
}
// 执行Kruskal算法
// 按边的权值对边进行从小到大排序
sort(edges.begin(), edges.end(), [](const Edge& a, const Edge& b) {
return a.val < b.val;
});
}
然后我给大家提供一下并查集模版,以后用的时候可以直接背o~
// 节点数量
int n = 10001;
// 并查集标记节点关系的数组
vector<int> father(n, -1); // 节点编号是从1开始的,n要大一些
// 并查集初始化
void init() {
for (int i = 0; i < n; ++i) {
father[i] = i;
}
}
// 并查集的查找操作
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩
}
// 并查集的加入集合
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}
最后就是从小到大遍历这些边,如果对应两个节点没有相连就用这条边,如果连过了,就跳过
// 从头开始遍历边
for (Edge edge : edges) {
// 并查集,搜出两个节点的祖先
int x = find(edge.l);
int y = find(edge.r);
// 如果祖先不同,则不在同一个集合
if (x != y) {
result_val += edge.val; // 这条边可以作为生成树的边
join(x, y); // 两个节点加入到同一个集合
}
}
cout << result_val << endl;
return 0;
完整代码展示
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// l,r为 边两边的节点,val为边的数值
struct Edge {
int l, r, val;
};
// 节点数量
int n = 10001;
// 并查集标记节点关系的数组
vector<int> father(n, -1); // 节点编号是从1开始的,n要大一些
// 并查集初始化
void init() {
for (int i = 0; i < n; ++i) {
father[i] = i;
}
}
// 并查集的查找操作
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩
}
// 并查集的加入集合
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}
int main() {
int v, e;
int v1, v2, val;
vector<Edge> edges;
int result_val = 0;
cin >> v >> e;
while (e--) {
cin >> v1 >> v2 >> val;
edges.push_back({v1, v2, val});
}
// 执行Kruskal算法
// 按边的权值对边进行从小到大排序
sort(edges.begin(), edges.end(), [](const Edge& a, const Edge& b) {
return a.val < b.val;
});
// 并查集初始化
init();
// 从头开始遍历边
for (Edge edge : edges) {
// 并查集,搜出两个节点的祖先
int x = find(edge.l);
int y = find(edge.r);
// 如果祖先不同,则不在同一个集合
if (x != y) {
result_val += edge.val; // 这条边可以作为生成树的边
join(x, y); // 两个节点加入到同一个集合
}
}
cout << result_val << endl;
return 0;
}