问题引入
考虑如下问题:有n个城市,要在这n个城市之间修建道路,显然只要修建n-1条路就能让任意两个城市连通,但每两个城市之间修建道路所花的费用不同,该如何选择使花费最少?
这个问题就是在具有n个点的连通图中选择n-1条边使图依然连通,并且花费最少,也就是构造图的最小生成树。
常用算法:
普里姆(Prim)算法和
克鲁斯卡尔(Kruskal)算法
Prim算法
算法描述
普里姆算法(Prim)先将图中任意一点加入空集U中,然后在 U与V-U的点之间的所有边中找到代价最小的边,选中,并把端点 加入U中,依次进行,直到选出n-1条边。
代码实现
#include<bits/stdc++.h>
using namespace std;
const int N=55;
const int inf=1e9;
struct Graph{
int vum;
int arcs[N][N];//邻接矩阵
};
void prime(Graph &G){
int n=G.vum,ans=0;//顶点个数,最小代价和
vector<bool> vis(n,false);//是否被访问过
vector<int> dis(n,inf);//当前剩下的结点到最小生成树的距离
vis[0]=true;//顶点0加到最小生成树中去
for(int i=1;i<n;i++){//距离初始化
dis[i]=G.arcs[0][i];//邻接矩阵中各个顶点到0之间的距离
}
for(int i=1;i<n;i++){
int m=inf,j=0;//到最小生成树的的最小距离 最小距离顶点的编号
for(int k=0;k<n;k++){
if(!vis[k]&&dis[k]<m){//如果k没被访问过,并且k到最小生成树的距离小于最小距离
m=dis[k];//更新最小距离
j=k;//更新顶点编号;
}
}
if(j==0){
cout<<"不存在最小生成树,图不连通"<<endl;
}
ans+=m;
vis[j]=true;
for(int k=0;k<n;k++){
if(G.arcs[j][k]<inf && !vis[k]){//如果其他结点到j的距离存在并且没有访问
dis[k]=min(dis[k],G.arcs[j][k]);//最新的距离就是原来距离和到j距离的的最小值
}
}
}
cout<<ans<<endl;
}
int main(){
Graph G;
cin>>G.vum;
for(int i=0;i<G.vum;i++){//输入邻接矩阵
for(int j=0;j<G.vum;j++){
cin>>G.arcs[i][j];
if(G.arcs[i][j]==0){
G.arcs[i][j]=inf;
}
}
}
prime(G);
return 0;
}
//输入:
//4
//0 2 4 0
//2 0 3 5
//4 3 0 1
//0 5 1 0
//输出:
//6
Kruskal算法
算法描述
克鲁斯卡尔(Kruskal)算法采用贪心思想,按边权值从小到大开始寻找,如果该边的两个端点属于不同的连通分量,则加入该边,否则舍弃该边。
并查集的应用
Kruskal算法主要利用并查集的思想:即在升序的边的集合中依次挑选最小的边,并检查该边的两个端点是否属于同一个集合,如果不属于则可以将两个端点合并到一个集合中去,并选取这条边;如果属于则不能合并,即不能选取这条边 。
例子:
代码实现
#include<bits/stdc++.h>
using namespace std;
const int N=105;//边的上限
struct edge{
int x,y;//顶点
int w;//权重
};
int fa[N],n;//并查集数组 实际顶点数
int getf(int x){//查询一个顶点在并查集中属于哪一个类别
return x==fa[x] ? x:fa[x]=getf(fa[x]);//如果相等,则它是最终类别;如果不是,则看它和谁一个类别,逐层查下去,直到查到最终类别。
}
void Kruskal(vector<edge> &e){
int ans=0;
for(int i=0;i<n;i++){//并查集初始化
fa[i]=i;
}
//核心代码
for(int i=0;i<e.size();i++){//e已经从小到大排序好了
int fx=getf(e[i].x),fy=getf(e[i].y);//看每一条边的两个顶点是否属于同一类别
if(fx!=fy){
ans+=e[i].w;
fa[fx]=fy;//将该顶点所属类别fx归并到fy中去,即x的最终类别就是fy了
}
}
cout<<ans;
}
bool cmp(edge a,edge b){//因为结构体是新定义的,所以要写一个比较函数
return a.w<b.w;
}
int main(){
while(cin>>n && n){
int m=n*(n-1)/2;//边的最大数目:完全图
vector<edge> e(m);//开最大的向量,防止越界
for(int i=0;i<m;i++){
cin>>e[i].x>>e[i].y>>e[i].w;
if(e[i].x==0 && e[i].y==0 && e[i].w==0){//输入结束的条件
break;
}
}
sort(e.begin(),e.end(),cmp);
Kruskal(e);
}
return 0;
}
//输入
//6
//1 2 6
//1 3 1
//1 4 5
//2 3 5
//2 5 3
//3 6 4
//3 4 6
//3 5 6
//4 6 2
//5 6 6
//0 0 0
//输出
//15