本篇博客来自南昌理工学院acm队成员
写作缘由:复习和重新练习最小生成树。
最小生成树
1.作者建议:
对于学习最小生成树最好是明白以下关于图的概念:
(1).连通图:在无向图中,若任意两个顶点vivi与vjvj都有路径相通,则称该无向图为连通图。
(2).强连通图:在有向图中,若任意两个顶点vivi与vjvj都有路径相通,则称该有向图为强连通图。
(3).连通网:在连通图中,若图的边具有一定的意义,每一条边都对应着一个数,称为权;权代表着连接连个顶点的代价,称这种连通图叫做连通网。
(4).
树:如果一个无向连通图不包含回路(连通图中不存在环),那么就是一个树。
生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环。
(5).最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。
2.作用
要在n个城市之间铺设光缆,主要目标是要使这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,因此另一个目标是要使铺设光缆的总费用最低。这就需要找到带权的最小生成树。(最小连通距离)
3.实现原理
(1).prim(普利姆算法)
算法原理:
逐渐“加点”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。
算法步骤:
1.初始化设所有点的距离到最小生成树的距离为最大,任意选一个点为最小生成树的初始点,距离为0;
2.开始寻找一个离没有建好的最小生成树最近的点,开始利用这个点进行更新其他点到最小生成树的距离。
3.迭代直到找到n个点或者是n-1条边。
图解:
代码实现:
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1000;
int dist[N],g[N][N],n,m,res=0; dist为距离,g存储图,n个点,m条边,res记录权值和。
bool str[N]; str记录每个点是否用过
int prim()
{
memset(dist,0x3f,sizeof dist);
dist[1]=0; 初始化
for(int i=0; i<n; i++) 遍历每条边
{
int t=-1;
for(int j=1; j<=n; j++)
{
if(!str[j]&&(t==-1||dist[t]>dist[j]))
{
t=j;
}
}
if(dist[t]==0x3f3f3f3f)return -1; 如果当前点的距离为最大值说明没有最小生成生树
res+=dist[t]; 累加权值
str[t]=true; 标记点已经用过
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,w;
cin>>a>>b>>w;
g[a][b]=min(g[a][b],w); 防止重边
g[b][a]=g[a][b];
}
int i=prim();
if(i==-1)
cout<<"impossible"<<endl;
else cout<<i;
return 0;
}
注意理解dist的含义,dist指的是距离最小生成树的距离,也就是最小生成树的边权值。
练习例题
(2).kruskal(克鲁斯卡尔算法)
算法原理:
逐渐“加边”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
算法步骤:
1.初始化所有点为0;
2.找到最小的边,并判断有没有构成环。
3.迭代n-1次,直到最小生成树所有边都遍历完了之后结束。
图解:
代码实现:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=2 *1e5+10;
int p[N]; 并查集的父亲节点
struct edge {
int a, b, w;
bool operator<(const edge &e) const {
return w < e.w; 比教,运算符的重载
}
} e[N];
int n,m;
int find(int x)
{
//并查集核心
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int kruskal() {
sort(e, e+m); 排序
for(int i = 1; i <= n; i++) p[i] = i; //初始化并查集
int res = 0, cnt = 0;
//res 最小生成树中的权重之和
//cnt 当前加了多少条边
for(int i = 0; i < m; i++) {
遍历所有边
auto t = e[i];
t.a = find(t.a), t.b = find(t.b);
if(t.a != t.b) {
如果不成环就合并
p[t.a] = t.b;
res += t.w; 记录边权值
cnt++;
}
}
if(cnt < n - 1)
{
cout<<"impossible"<<endl;
return -1; 如果没有n-1条边就说明没有最小生成树
}
cout<<res<<endl;
return res;
}
int main()
{
cin>>n>>m;
for(int i=1; i<=n; i++)
p[i]=i; 并查集初始化
for(int i=0; i<m; i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
e[i]= {
a,b,c};
}
//for(int i=0;i<m;i++)cout<<e[i].w<<endl;
kruskal();
return 0;
}
注意并查集的作用是判环,原理是父亲节点都相同。
习题练习
总结与累积:
最小生成树作者(acmer)已经学完两个多月了,但是其中的许多细节还是有些不清楚,依旧需要翻模板或者是花费许多时间,在学习图论时,需要相当清楚每一个算法原理,然后依据原理来进行每一步操作,当然每一步操作都是需要之前学习基础算法的实现,例如并查集,堆优化等等,还有数据结构的基础。当然还有每一个算法的实现背景以及会出现什么不能构成算法的因素,这些都是需要考虑的,所以学好一个算法需要深刻的理解和全方位的明白,以及大量的训练。
话说看到我的文章可能写的比较学术化,有些枯燥无味,但是这是学习,忍忍。
奖励看完那些看完我的文章的帅哥们(lsp):