目录
最小生成树简介
什么是树
树(tree)是一种特殊的图,一个图要成为树要满足三个条件:
- 该图是一个无向图(准确意义上来说,有根树的父子节点间的关系也可以算是有向边)
- 该图连通(即图上任意两点都可以互相到达)
- 该图无环(即图上任意两点间有且只有一条简单路径)
在树的这三条要求中,后面两条很重要,经常会用到,至于第一条并不是那么重要,大家了解下就行了,等大家会用了自然就不用管它了。
关于树还有一些专有名词需要大家记一下:
- 根节点(root):代表一个有根树中深度最小的节点
- 有根树:代表一个指定了根的树
- 无根树:代表一个没有指定根的树
- 叶节点(leave):代表一个有根树没有子节点的节点
- 父节点(father):代表一个有根树中与所指节点有边相连,且深度比它小1的那个节点
- 子节点(son):代表一个有根树中与所指节点有边相连,且深度比它大1的那些节点
- 度(degree):代表一个有根树中一个节点所拥有的子节点的个数(这里跟图不一样,图中是与该节点有边相连的节点的个数
- 深度:代表一个有根树中一个节点到根的距离
除此之外树还有些很常用的性质:
- 树中点的个数总是比边的个数多1
- 根节点没有父节点
- 树中节点的度数之和等于其边数
听着还是很抽象吧,那接下来我给你们上几张图
看,这就是一个无根树它显然满足我所说的要求:无环且连通
然而,当我给这颗树指定一个根时,它就变成了这样
其中1是整棵树的根。这棵树的层次关系就一目了然了,打个比方其中6是4的子节点,4是6的父节点,4的度为3,2的度为2;6,7,5,8,9是整棵树的叶节点,而3的深度为1,8的深度为3。
什么是生成树
生成树(spanning tree)是一个图的子图,它的定义如下
- 它是一颗无根树
- 它包含原图的所有节点
听着可能有点抽象,说大白话,就是他是一个图删掉若干条边形成的树
当然,一个图的生成树可能有很多个,比如下面这个例子:
其中黑色的边是原图,红色的边是它的一个生成树,蓝色的则是它的另一种生成树
什么是最小生成树
最小生成树(minimum spanning tree)其实就是一个生成树,不过它不同于一般的生成树,它的边权之和是最小的,即边权和最小的生成树,准确的来说,同一个图的最小生成树也可以有很多个,但是其边权和肯定是一样的
举个栗子
这个图的最小生成树是什么呢?
对的,是这样的
这是这张图的最小生成树,它的边权和是10。
而最小生成树作为一个问题,一般都会询问最小生成树的边权的最大值或者边权之和。
最小生成树的做法
Kruscal(克鲁斯卡尔)算法
思路
Kruscal的思路呢,其实是基于贪心的。
具体做法呢,就是按照下面几个步骤来
- 把图上的每一条边存在一个数组里,数组的每个元素应有(起点,边权,终点)三个数据
- 将该边数组按边权从小到大排序
- 依次按边的边权从小到大枚举每一条边,如果边的两个端点已经连通了,那就跳过这条边(通过并查集判断)
- 否则把总答案累计上这条边
- 用并查集merge这条边的两个端点
- 返回第3步
Kruscal算法中有用到并查集,不知道并查集是啥的可以看看我之前发的blog。
代码
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 110; // 最大顶点数
const int maxm = 10010; // 最大边数
struct Edge {// 使用结构体储存每一条边,便于排序
int u, v, w; // 表示有一条 (u,v) 的无向边,边权为 w
} e[maxm];
int ecnt;// 用于边表计数
void addEdge(int u, int v, int w){ // 加入一条无向边
++ecnt;
e[ecnt].u = u;
e[ecnt].v = v;
e[ecnt].w = w;
}
int fa[maxn]; // 并查集相关
int find(int x) {
return x == fa[x] ? x : fa[x] = find(fa[x]); // 路径压缩
}
int n; // 顶点数
bool cmp(const Edge &a, const Edge &b){
return a.w < b.w;
}
int Kruskal() { // Kruskal 算法核心过程
for (int i = 1; i <= n; i++) {
fa[i] = i; // 初始化并查集
}
sort (e + 1, e + ecnt + 1, cmp);
int sum = 0;
for (int i = 1; i <= ecnt; i++) {
int u = e[i].u;
int v = e[i].v;
u = find(u);
v = find(v);
if (u != v) {
fa[u] = v;
sum += e[i].w;
}
}
return sum;
}
int main(){
scanf("%d",&n);
int w;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
scanf("%d", &w);
addEdge(i, j, w);
}
}
int ans = Kruskal();
printf("%d\n", ans);
return 0;
}
这个代码的输入一个整数表示节点个数,而后面则输入一个邻接矩阵。输出的是该图的最小生成树的边权和
对了,忘记说了,Kruskal的时间复杂度为O(mlogm),其主要的时间花费在给边排序上,其中m为边数,所以在使用的时候要注意下数据范围哦。
其他算法
关于最小生成树还有两种算法,我这里就不介绍了,大家感兴趣的话可以去搜一搜,我自己是觉得Kruscal已经足够优秀了,所以我就基本上都用Kruscal了,况且另外两种算法:Prim以及Boruvka,他们的时间复杂度也不必Kruscal优秀,所以写代码的话Kruscal是有必要掌握一下的。
那我今天就讲到这里,Bye~