最小生成树详解

目录

最小生成树简介

什么是树

什么是生成树

什么是最小生成树

最小生成树的做法

Kruscal(克鲁斯卡尔)算法

思路

代码

其他算法


最小生成树简介

什么是树

树(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的思路呢,其实是基于贪心的。

具体做法呢,就是按照下面几个步骤来

  1. 把图上的每一条边存在一个数组里,数组的每个元素应有(起点,边权,终点)三个数据
  2. 将该边数组按边权从小到大排序
  3. 依次按边的边权从小到大枚举每一条边,如果边的两个端点已经连通了,那就跳过这条边(通过并查集判断)
  4. 否则把总答案累计上这条边
  5. 用并查集merge这条边的两个端点
  6. 返回第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~

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值