@算法
最小生成树
从徐州铁首归来之后终于被安排上了讲课,周二要讲最小生成树,为了避免讲课3分钟,发呆两小时,其实是为了打发一下上午的时间 想试试看写一篇算法博客。
最小生成树的百度定义:一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。
与之相关的有两个我刚学会的 算法。
克鲁斯卡尔
若有n点m边
时间复杂度 O(mlogm)
克鲁斯卡尔的算法和并查集算法大同小异。n个节点可以看作n个独立的集合,最小生成树就是要从给出的边中挑选一些边,把孤立的集合连接到一起。
以新生
并查集裸题
为例。
此题提前给你一些边之后问你要添加多少条边才能把所有的点连接到一起。
如果给0条边,则可以看作是每条边权为1的最小生成树问题,AC源码不用改还能接着AC。
但是
但是
但是
而如果加上边权(之前默认为1),就会出现两个问题。
- 边权怎么记录
- 两个集合不一定让祖先连接到一起最优(祖先之间的距离或许很大)
所以先前枚举点来连接的方法不再适用,但是把n个点组合到一个集合的思想是不变的。
边权怎么记录?
用结构体保存两个端点以及权值
struct EDGE{
int from,to,cost;
}edge[maxn];
如何连接两个独立的集合?
按题意我们需要找到两个集合中最小的边,才能最优的连接两个集合。
如何找到两个集合中最小的边?
枚举集合再从中找到最小的边很困难,不妨换种思路。
枚举边!
只需要把边从短到长排序,每次拿出最小的边,判断一下它的两个端点是否在一个集合内,如果不在则用当前边连接两个端点。这样可以保证每次连接的边都是两个集合中最短的边。
以上克鲁斯卡尔就讲完了,什么?怎么证明,你问测评机A不A,A了就是对了。有兴趣的同学可以自己思考。
例题
还是通畅工程
裸题不解释,直接上代码。
#include<stdio.h>
#include<algorithm>
using namespace std;
const int maxn = 105 * 105;
int father[maxn];
struct ROAD{
int u, v, c;
}r[maxn];
int cmp(ROAD a,ROAD b){
return a.c < b.c;
}
int find(int x){
if(father[x] == x)return x;
else father[x] == find(father[x]);
}
int main(){
int n, m;
int u, v, c;
int ans;
while(~scanf("%d", &n) && n != 0){
ans = 0;//多组答案清零
for(int i = 1 ; i <= n ; i++)father[i] = i;//重置father数组
m = (n - 1) * n / 2;//计算出路的个数
for(int i = 1 ; i <= m ; i++)scanf("%d %d %d",&r[i].u, &r[i].v, &r[i].c);//读入
//------------------克鲁斯卡尔----------------------------------------------------------
sort(r + 1, r + 1 + m , cmp);
for(int i = 1 ; i <= m ; i++){
if(find(r[i].u) != find(r[i].v)){
father[find(r[i].u)] = find(r[i].v);
ans += r[i].c;
}
}
//--------------------------------------------------------------------------------------
printf("%d\n",ans);
}
}
普里姆
时间复杂度O(n^2)
Dijkstra算法亲兄弟。
最早碰到最小生成树就靠着学过的Dij算法改版子过的。
大概思想
- 把点分成两部分——已连接的和没有连接的。(初始已连接的点只有一个起点)
- 在所有与已连接的点相邻的点中选一个距离起点长度最短的点;
- 把挑选出的归到已连接的部分中,并算出所需的边长加入答案。
4. 更新与之相邻的所有点到起点的长度(放缩)。 - 重复第二步,直到所有的点都被连接。
和Dijkstra过于相像,代码可以自行脑补💪
总体而言,克鲁斯卡尔按边排序,适用于边少最小生成树。
普里姆按点展开,适用于点少边多的最小生成树。
附赠祖传练习题连接