最小生成树算法框架
什么是最小生成树?
一个图有 n 个结点,很多条边。最小生成树需要包含原图中的 n 个结点,并且有保持图连通的最少的边,且使这个树的权值达到最小。
遇到稠密图就用朴素版prim,稀疏图就用kruskal即可,堆优化版的prim没有必要掌握。
朴素版prim
朴素版 prim 和 dijkstra 算法很像。
dijkstra 中的 s集合是确定最短路的点
prim 中的 s集合是已经被加入到最小生成树的点
这里的集合s代表 在当前已经被加入到最小生成树的点。
每循环一次,都可以将一个点加入 s,循环 n 次后,所有点都可以加入最小生成树了。
举个例子: 求下图的最小生成树:
① 我们选 A 为起点,将 A 加入集合s
② 集合s为{A},与 s 最近的是 D,将 D 加入s
③ 集合s为{A、D},与 s 最近的是 B,将 B 加入s
④ 集合s为{A、B、D},与 s 最近的是 E,将 E 加入s
⑤ 集合s为{A、B、D、E},与 s 最近的是C,将 C 加入s
耶!所有点都进集合了!我们的最小生成树就得到了:)
例题:AcWing 858. Prim算法求最小生成树
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 510,INF = 0x3f3f3f3f;
int n,m;
int g[N][N]; // 邻接矩阵的存储
int dist[N]; // 存储点到 集合s 的最短距离
bool st[N]; // true为在 集合s 内
int prim()
{
memset(dist,0x3f,sizeof(dist));
int res = 0; //最小生成树的所有边的权重之和
for(int i=0; i<n; i++)
{
int t = -1;
for(int j=1; j<=n; j++) //找到当前在 集合s外 且距离 集合s 最小的点t
if(!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
if(i && dist[t] == INF) return INF; //当前图不连通,不存在最小生成树
if(i) res += dist[t]; //将这个点与集合s中的某相连的点的权值加入答案
for(int j=1; j<=n; j++) dist[j] = min(dist[j],g[t][j]); //用 t 更新其他点到集合的距离
st[t] = true; //将 t 加入 集合s
}
return res;
}
int main()
{
cin >> n >> m;
memset(g,0x3f,sizeof(g));
while(m--)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
g[a][b] = g[b][a] = min(g[a][b],c); //无向图相反要建两条变 ,重边的话只取权值最小的
}
int t = prim();
if(t == INF) cout<<"impossible";
else cout<<t;
}
注意: 下面这两部顺序不能颠倒的! 要先把那个边的权值加入答案中,再去更新其他点。因为可能出现自环。
if(i) res += dist[t]; //将这个点与集合s中的某相连的点的权值加入答案
for(int j=1; j<=n; j++) dist[j] = min(dist[j],g[t][j]); //用 t 更新其他点到集合的距离
举个例子哈 :)
刚开始集合里是A,A将B的dist更新为3,当B进入集合,我们要用B去更新其他点的到集合的最短距离时,B会将自己更新为-2,更新后再将-2加入答案是不对的,因为我们要求的是最小生成树,树是不能有环的,所以要先将3加入答案,再用B更新!!!
kruskal
此算法需要用到并查集!排序用 sort 即可。sort的时间复杂度为O(mlogm)
举个例子: 求此图的最小生成树
对所有边排序后,刚开始所有点都是不连通的。
① 枚举最小边A——D,A与D不连通,合并A与D
② 枚举B——E,B与E不连通,合并B与E
③ 枚举B——C,B与C不连通,合并B与C
④ 枚举A——C,A与C不连通,合并A与C
⑤ 枚举D——E,D与E连通,不合并
⑥ 枚举A——B,A与B连通,不合并
⑦ 枚举C——E,C与E连通,不合并
⑧ 所有边都已经枚举过了,最小生成树就得到了:)
例题:AcWing 859. Kruskal算法求最小生成树
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 2e5+10;
int n,m;
int p[N]; // 并查集的p数组
struct Edge
{
int a,b,w; // a—>b的边权值为w
}edges[N];
bool cmp(Edge a,Edge b)
{
return a.w<b.w;
}
int find(int x) //并查集
{
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
void kruskal()
{
sort(edges,edges+m,cmp); //排序
for(int i=1; i<=n; i++) p[i] = i; //初始化并查集
int res = 0, cnt = 0;
for(int i=0; i<m; i++) //从小到大枚举所有边
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
if(find(a) != find(b)) //判断 a 和 b 是否连通
{
p[find(a)] = b; //合并
res += w; //最小生成树的所有树边权值之和
cnt++; //当前最小生成树的边数
}
}
if(cnt < n-1) cout<<"impossible"; //小于 n-1 说明不连通
else cout<<res;
}
int main()
{
cin >> n >> m;
for(int i=0; i<m; i++)
{
int a, b, w;
scanf("%d%d%d",&a,&b,&w);
edges[i] = {a,b,w};
}
kruskal();
return 0;
}