赞
踩
今天我们介绍图论中的另一部分,最小生成树。
对图的最短路感兴趣的同学可以看:
【图解算法】一次解决最短路径问题
用直白的话来说,就是给定一个无向图,在图中选择若干条边把图的所有的节点连接起来,要求边长之和最小。在图论中,叫做最小生成树。
举个例子:
我们余姚在n个城市之间铺设光缆,使它们之间都可以通信。但是铺设光缆的费用很高,且铺设光缆的费用与距离成正比,那么我们应该如何铺设光缆才能使总费用最低呢?
每次将距离已经连通部分最近的点 和对应的边 加入连通部分,是连通部分逐渐扩大,最后将整个图连接起来并且边长之和最小。
伪代码:
int dist[n],state[n],pre[n];
dist[1] = 0;
for(i : 1 ~ n)
{
t <- 没有连通起来,但是距离连通部分最近的点;
state[t] = 1;
更新 dist 和 pre;
}
这样讲有点抽象,我们配合图举个例子:
首先,我们设置:
假定我们从1号节点开始扩充 连通集,所以 1 号节点与连通部分的最短距离为 0,即disti[1] 置为 0。
遍历 dist 数组,找到一个还没有连通起来,但是距离连通部分最近的点,假设该节点的编号是 i。i节点就是下一个应该加入连通部分的节点,stata[i] 置为 1。
如果我们用灰色表示尚未连通的点,绿色表示在连通集中的点,那么此时距离最小的显然是 1号节点,故state[1]置1.
接着,我们遍历节点1 的 所有可达点j,如果 j 距离连通部分的距离大于 i j 之间的距离,dist[j] > w[i][j](w[i][j] 为 i j 节点之间的距离),则更新 dist[j] 为 w[i][j]。这时候表示,j 到连通部分的最短方式是和 i 相连,因此,更新pre[j] = i。
与节点 1 相连的有 2, 3, 4 号节点。1->2 的距离为 100,小于 dist[2],dist[2] 更新为 100,pre[2] 更新为1。1->4 的距离为 140,小于 dist[4],dist[4] 更新为 140,pre[2] 更新为1。1->3 的距离为 150,小于 dist[3],dist[3] 更新为 150,pre[3] 更新为1。
我们之后,只需要不断重复上两步,直到所有节点都被加入了连通集:
此时dist数组中保存的就是各个节点需要修的路,加起来就是总长度。pre数组中保存了需要选择的边
#include <cstring> #include <iostream> #include <algorithm> using namespace std; const int N=510; int g[N][N]; bool st[N];//记录已经在集合中的点 int dist[N];//记录各个点到集合的距离 // int pre[N]; //该模板题没有要求输出所有边,所以不需要定义prev数组 int n,m; int Prim() { int ans=0; 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(!st[j]&&(t==-1||dist[j]<dist[t])){ t=j; } } //判断当前节点是否与集合联通,不连通则说明没有最小生成树 if(i && dist[t] == 0x3f3f3f3f) return 0x3f3f3f3f; ans+=dist[t]; st[t]=true; for(int j=1;j<=n;j++){ if(dist[j]>g[t][j]&&!st[j]){ dist[j] = g[t][j]; //pre[j] = t;//从 t 到 j 的距离更短,i 的前驱变为 t. } } } return ans; } //void getPath()//输出各个边 //{ // for(int i = n; i > 1; i--)//n 个节点,所以有 n-1 条边。 // // { // cout << i <<" " << pre[i] << " "<< endl;// i 是节点编号,pre[i] 是 i 节点的前驱节//点。他们构成一条边。 // } //} int main() { cin>>n>>m; memset(g,0x3f,sizeof g); while(m--){ int a,b,w; cin>>a>>b>>w; g[a][b]=g[b][a]=min(g[a][b],w); } int t=Prim(); if (t == 0x3f3f3f3f) puts("impossible"); else cout<<t; }
与Dijkstra算法类似,Prim算法也可以使用堆来优化,优化之后时间复杂度由O(n^2)降为O(mlogN).适用于稀疏图,但是稀疏图的时候,其实使用 Kruskal算法更加实用。
所以,这里我们不讲解优化算法,感兴趣的同学可以参照Dijkstra算法的优化来实现。
此时筛选出来的边和所有的顶点构成此连通网的最小生成树。
对于判断是否会产生回路,我们使用并查集。
不懂并查集的小伙伴可以看这位大佬的文章,简单易懂:
【算法与数据结构】—— 并查集
我们依然是画图来演示一下:
我们已经将边按照权值做了升序排列
2. D-T 边不会和已选 B-D 边构成环路,可以组成最小生成树:
A-C 边不会和已选 B-D、D-T 边构成环路,可以组成最小生成树:
C-D 边不会和已选 A-C、B-D、D-T 边构成环路,可以组成最小生成树:
C-B 边会和已选 C-D、B-D 边构成环路,因此不能组成最小生成树:
如图下图 所示,对于一个包含 6 个顶点的连通网,我们已经选择了 5 条边,这些边组成的生成树就是最小生成树。
#include <iostream> #include <cstring> #include <algorithm> using namespace std; const int N = 100010; int p[N];//保存并查集 struct E{ int a; int b; int w; bool operator < (const E& rhs){//通过边长进行排序 return this->w < rhs.w; } }edg[N * 2]; int res = 0; int n, m; int cnt = 0; int find(int a){//并查集找祖宗 if(p[a] != a) p[a] = find(p[a]); return p[a]; } void klskr(){ for(int i = 1; i <= m; i++)//依次尝试加入每条边 { int pa = find(edg[i].a);// a 点所在的集合 int pb = find(edg[i].b);// b 点所在的集合 if(pa != pb){//如果 a b 不在一个集合中 res += edg[i].w;//a b 之间这条边要 p[pa] = pb;// 合并a b cnt ++; // 保留的边数量+1 } } } int main() { cin >> n >> m; for(int i = 1; i <= n; i++) p[i] = i;//初始化并查集 for(int i = 1; i <= m; i++){//读入每条边 int a, b , c; cin >> a >> b >>c; edg[i] = {a, b, c}; } sort(edg + 1, edg + m + 1);//按边长排序 klskr(); if(cnt < n - 1) {//如果保留的边小于点数-1,则不能连通 cout<< "impossible"; return 0; } cout << res; return 0; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。