算法总结之最小生成树
首先先来弄清楚几个定义
(1)连通图:在无向图中,任意两点都有路径相通,也就是任意两点都可以走到,则称该图为连通图。
(2)强连通图:在有向图中,任意两点都有路径相通,则称为强连通图。
(3)生成树:n个点,有且只有n-1条边的连通图,称为生成树。生成树不存在环。
(4)最小生成树:边权之和最小的生成树,称为最小生成树。
(5)稠密图:边的数量大约是点的数量的平方级别,一般用邻接矩阵来存。
(6)稀疏图:边的数量小于点的数量的平方级别,一般用邻接表来存。
求最小生成树一般有两种方法,普利姆算法和克鲁斯卡尔算法。两种算法各有优点,一般分别对应于稠密图和稀疏图。
对于给定的n个点,m条边,我们需要选择n-1条构成最小生成树。
(1)普利姆算法:
思想核心:通过迭代n次,每次选择一个集合外距离集合最近的点(这里的集合是指已经完成连通的点的点集),加入集合,用这个点去更新其他点到集合的的距离。因为第一次迭代集合中还没有点,所以可以选择任意一个点作为“初始点”。具体代码如下:(直接在网页上写的代码,可能会有些地方手残代码敲错)
#include <bits/stdc++.h>
using namespcace std;
int g[N][N];//邻接矩阵,用来存图
int n,m;//n个点,m条边
bool st[N];//纪录某个点又没有加入集合
int dis[N];//纪录距离
int prim()//算法实现
{
memset(dis,0x3f,sizeof dis);//初始化距离
int res=0;
for(int i=0;i<n;i++)//迭代n次
{
int t=-1;
for(int j=1;j<=n;j++)//找到集合外距离集合最近的点,st[j]==1表示已经加入集合
{
if(!st[j]&&(t==-1||dis[j]<dis[t]))t=j;
}
// 当i==0是,是第一次迭代,随便找一个点
if(i&&dis[t]==inf)return inf;//不连通的情况
if(i)res+=dis[t];//先加再更新其他点,以防存在有坑的数据存在自环并且权重为负值,这样会把本身的距离变的更小,但根据生成树的定义是没有自环的。
for(int j=1;j<=n;j++)dis[j]=min(dis[j],g[t][j]);//用找到的点去更新集合外的点到集合的最小距离
st[t]=1;//加入集合
}
}
int main()
{
memset(g,0x3f,sizeof g);//某两点之间不一定只读入一条边,保留距离最小的
//读入数据,不具体写了。注意是无向图
int t=prim();
if(t==0x3f3f3f3f)...//不连通,无法形成最小生成树
else cout<<t<<"\n";//输出最小生成树的边权之和
return 0;
}
(2)克鲁斯卡尔算法:
思想核心:将所以边的权重按从小到大排序,然后遍历所有边,如果两个点不连通,就把他们连通,对于已经连通的,因为是按权重从小到大枚举的,所以在之前进行连通时更新的是最优的,判断最后连通的边是否等于n-1就行了。维护连通块连通性显然用并查集,具体看代码:
#include <bits/stdc++.h>
using namespace std;
int n,m;
struct node{
int u,v,w;
bool operator <(const node &W)const //结构体自定义排序
{
return w<W.w;
}
}a[N]; //存边
int fa[N]; //并查集
int find(int x)
{
if(fa[x]!=x)return fa[x]=find(fa[x]);
return fa[x];
}
int main()
{
//先读入数据,不具体写了
for(int i=1;i<=n;i++)fa[i]=i;//并查集初始化
int cnt=0;//纪录连通的边数
int res=0;//纪录权重之和
for(int i=0;i<m;i++)
{
int u=a[i].u,v=a[i].v,w=a[i].w;
int pu=find(u),pv=find(v);
if(pu!=pv)
{
cnt++;
res+=w;
fa[pu]=pv;//使其连通
}
}
if(cnt<n-1)cout<<-1<<"\n"; //根据生成树的定义,n个点,n-1条边。
else cout<<res<<"\n";
return 0;
}