一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。 最小生成树可以用prim(普里姆)算法或kruskal(克鲁斯卡尔)算法求出。本文以18448为例,介绍这两种算法的具体实现。
(一)Prim算法
设定图结构的结点集合V,边集合E。Prim算法设最小生成树的结点集合U开始为空集,任选V中任意一个结点u放入集合U,此时在集合U和集合V-U中选择满足条件的边,即边(x,y)的两个点x在集合U中,而y在集合V-U中,满足这个条件边中选择权值最小的,这条边一定属于一棵最小生成树,然后将结点y也放入集合U中。将这个过程重复n-1次,可得最小生成树的n-1条边。
算法的实现一些细节:(1)图结构算法几乎都需要使用标志数组v,用来标记已经访问过的结点。此处标志数组也能用于表示集合U(标记为1元素)和集合V-U(标记为0元素);(2)d数组用于辅助找到满足条件的最小的边,通过迭代处理的方式更新数组d。可通过上图和代码来理解处理过程。(如果想算法优化可用优先队列替换数组d)
邻接矩阵写法:
#include <iostream>
#include <cstring>/**< memset函数 */
using namespace std;
long long n,m,v[2005],d[2005],e[2005][2005];
int getMin()
{
int i,mini=0;/**< d[0]很大 */
for(i=1;i<=n;i++)
if(d[mini]>d[i]&&v[i]==0)
mini=i;
return mini;
}
long long prim()
{
long long ans=0;
memset(d,127/3,sizeof d);/**< 一个函数用于赋数组每个元素极大值 */
d[1]=0;/**< 确保第一次选择出1号结点 */
for(int j=1;j<=n;j++)
{
int minnode=getMin();
v[minnode]=1;
ans+=d[minnode];
for(int i=1;i<=n;i++)
d[i]=min(d[i],e[minnode][i]);
}
return ans;
}
int main()
{
long long i,j,x,y,z;
memset(e,127/3,sizeof e);/**< 邻接矩阵初始值为无穷大 */
cin>>n>>m;
for(i=1;i<=m;i++)
{
cin>>x>>y>>z;
if(x==y)continue; //注意可能存在重边和自环。
e[x][y]=e[y][x]=min(e[x][y],z);
}
cout<<prim();
return 0;
}
邻接表写法:
#include <iostream>
#include <vector>
using namespace std;
struct node /**< 边的邻接点和权值,也可用pair */
{
int adj,quan;
};
vector<node> e[2005];
int n,m,v[2005],d[2005];
int getMin()
{
int i,minv=0;
for(i=1;i<=n;i++)
if(v[i]==0&&d[i]<d[minv])
minv=i;
return minv;
}
int prim()
{
int i,j,ans=0;
for(i=0;i<=n;i++)
d[i]=1e9;
d[1]=0;
for(i=1;i<=n;i++)
{
int x=getMin();
v[x]=1; ans+=d[x];
for(j=0;j<e[x].size();j++)
{
int y=e[x][j].adj,z=e[x][j].quan;
d[y]=min(d[y],z);
}
}
return ans;
}
int main()
{
int i,x,y,z;
cin>>n>>m;
for(i=1;i<=m;i++)
{
cin>>x>>y>>z;
if(x==y) continue;
e[x].push_back({y,z});
e[y].push_back({x,z});
}
cout<<prim();
return 0;
}
(二)Kruscal算法
克鲁斯卡尔算法应用“贪心思想”。每次选择权值最小的边,但有一个附加条件,这次选择的边不能和已有的边形成环。经过n-1次选择可得最小生成树。
算法实现细节:(1)需要对所有的边按权值进行排序;(2)如何确保新选择的边不和已有的边形成环 ?先说一个笨办法,将选中边存起来,写一个搜索算法来检查新选择的边的两个结点是否连通,不连通就可以留下。而标准做法是使用并查集。并查集实际上是一种非常简单的树结构,能处理数据元素集合的查询和合并(两个集合的合并)。此处我们先假定图中n个结点初始为n个集合(每个结点是一个集合),如果选中一条边(x,y),那么将x所属集合和y所属集合进行合并。并查集的写法不是本文的重点,因此不做过多阐述。
#include <iostream>
#include <algorithm>
using namespace std;
int n,m,f[2005];/**< f数组用于并查集 */
struct node
{
int s,e,v;/**< s和e为两个邻接点,v为边权值 */
bool operator <(const node y)const
{
return v<y.v;
}
} e[20000];
int findx(int x)/**< 并查集的查操作 */
{
return f[x]==x?x:f[x]=findx(f[x]);
}
int main()
{
int i,j,sum=0;
cin>>n>>m;
for(i=1; i<=n; i++) /**< 并查集初始化 */
f[i]=i;
for(i=1; i<=m; i++)
cin>>e[i].s>>e[i].e>>e[i].v;
sort(e+1,e+m+1);
for(i=1; i<=m; i++)
{
int r1=findx(e[i].s),r2=findx(e[i].e);
if(r1!=r2)/**< 第i条边不会形成环,选中 */
{
sum+=e[i].v;
f[r1]=r2;/**< 并查集的并操作,将r1集合并入r2集合 */
}
}
cout<<sum;
return 0;
}