最小生成树
众所周知, 树是一种特殊的图, 是由n-1条边连通n个节点的图.
如果在一个有n个节点的无向图中, 选择n-1条边, 将n个点连成一棵树, 那么这棵树就是这个图的一个生成树.
如果保证树的边权和最小, 那么这棵树就是图的最小生成树.
为了求一棵树的最小生成树, 有两种算法, 一种是选择点加入树的Prim算法, 另一种是选择边加入树的Kruskal算法.
Prim算法
这个算法的过程和Dijkstra类似, 但有所不同.
首先选择任意一点作为树的第一个节点0, 枚举与它相连的所有点i, 将两点之间的边权记为这个点到生成树的距离b[i], 选择距离最近的点加入生成树, 然后枚举与之相邻的节点j, 用边权a[i,j]更新b[j], 使其等于min(b[j],a[i,j]), 这样再继续加入当前离生成树最近的点, 在更新它相邻的点, 以此类推, 直到所有点全部加入生成树. 这样, 便求出了最小生成树.
关于正确性
我自己的思路是这样的: 如果用Prim算法求出了一棵最小生成树, 将一条边u换成另一条更小的v, 就得到一棵边权和更小的生成树. 首先保证树连通, 所以去掉u和v, 生成树被分成两个连通块是一模一样的. 在当时连接u的时候, 已经决策完的生成树一定也和v相连, 这时v连接的节点一定会比u连接的节点更早加入, 所以一开始的假设不成立, 算法正确.
具体代码实现
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
int n,m,l,r,x,a[5005][5005]/*邻接矩阵*/,b[5005]/*点到生成树的最短边权*/,now/*当前加入的点*/,k=1/*生成树节点数*/,ans=0/*生成树总边权和*/;
bool vsd[5005]={0};
void update(int at){//用节点at更新其他点的b[]值
for(int i=1;i<=n;i++) {
b[i]=min(a[at][i],b[i]);
}
vsd[at]=true;
return;
}
int find(){//寻找当前离生成树最近的点
int ft=0;
for(int i=1;i<=n;i++){
if(!vsd[i]){//不在树中
if(b[i]<=b[ft]){
ft=i;
}
}
}
return ft;
}
int main(){
cin>>n>>m;
memset(a,0x3f,sizeof(a));
for(int i=1;i<=n;i++){
a[i][i]=0;
}
for(int i=1;i<=m;i++){
cin>>l>>r>>x;
a[l][r]=min(a[l][r],x);//防止有两个点之间出现边权不同的几条边
a[r][l]=min(a[r][l],x);
}
memset(b,0x3f,sizeof(b));
update(1);
while(k<n){//加入n-1个点后返回(第一个点本来就在树中, 无需加入)
now=find();//加入最近的点now
ans+=b[now];//统计答案
update(now);//更新其他点
k++;//统计点数
}
cout<<ans<<endl;
return 0;
}
Kruskal算法
这个算法和Prim相反, 它是将边记为树上的边, 最终得到一棵最小生成树.
将所有边按边权排序, 然后将它们从小到大讨论是否加入生成树. 如果该边的两个端点属于同一个连通块, 这时加入该边就会形成环, 不符合树的定义, 所以舍弃. 如果该边两个端点不属于同一个连通块, 那么连接该边, 将两个端点所在连通块连成一个.
当共加入n-1条边的时候, 就得到了一棵最小生成树.
对于查找两点是否在同一个连通块中的方法, 我们可以使用并查集来维护点之间的连通关系.
正确性简易说明
Kruskal相对来说更好理解, 因为从小到大排序后, 使用被舍弃的边连成环是非法的, 使用排在后面的合法的边替换已经选择的边, 得到的答案不是最优的. 所以Kruskal算法正确.
代码实现
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int n,m,fa[10005],s,e,l,k=0,ans=0;
struct side{
int le,ri,len;//起点, 终点, 边权
}a[200005];
bool cmp(side x,side y){//结构体sort规则
return(x.len<y.len);
}
int find(int x){//并查集寻找最老祖先
if(fa[x]==x){//自己就是当前连通块最老祖先
return x;
}
fa[x]=find(fa[x]);//自己祖先的最老祖先
return fa[x];
}
int main(){
cin>>n>>m;
memset(a,0x3f,sizeof(a));
for(int i=1;i<=m;i++){
cin>>s>>e>>l;
a[i].le=s;//结构体存储边
a[i].ri=e;
a[i].len=l;
}
sort(a+1,a+m+1,cmp);//按边权升序排列
for(int i=1;i<=n;i++){
fa[i]=i;//初始化并查集
}
int i=0;
while((k<n-1/*加入了n-1个点跳出*/)&&(i<=m/*枚举完了所有的边跳出*/)){
i++;
int fa1=find(a[i].le),fa2=find(a[i].ri);//两个端点的最老祖先
if(fa1!=fa2){//不在同一连通块
ans+=a[i].len;//记录答案
fa[fa1]=fa2;//连接连通块
k++;//记录边数
}
}
cout<<ans<<endl;
return 0;
}
之前发的是笔记, 现在发的是实战总结