最小生成树MST详解_mst最小生成树-程序员宅基地

技术标签: OI历程  算法  c++  数据结构  

前言

最近刚学了最小生成树,于是想趁热打火,先来总结一下~

前置芝士

  • 图、树的概念、遍历与存储
  • 并查集

本文章所有代码均以C++编写。

最小生成树的概念

最小生成树(Minimum Spanning Tree, MST)是一种特殊的图。它具备朴素树的所有性质,但也是一张图中边权最小但经过每个节点的子树。

定义

一个有 n n n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n n n 个结点,并且有保持图连通的最少的边。最小生成树可以用Kruskal(克鲁斯卡尔)算法或Prim(普里姆)算法求出。[1]

大意

在一张连通图 G G G 里,有 n n n 个节点和 m m m 条边,第 i i i 条边的权值为 w i w_i wi 。我们设图 G G G 的最小生成树为 T T T

那么,图 G G G 的最小生成树 T T T 必须具备以下条件:

  • T T T 必须包括 G G G 的所有节点;
  • T T T 的边数必须等于 n − 1 n - 1 n1
  • T T T 的边权和必须在所有生成树中最小;
  • T T T 必须为 G G G 的子图。

假设我们有如下的连通图 G G G(左),那么它的最小生成树 T T T 如图所示(右)。

最小生成树
[2]

显然对于图 G G G T T T 的边权和在所有生成树中最小。我们称这样的树为图 G G G最小生成树(简称MST)。

算法

我们不妨来介绍两种较为常见的求最小生成树的算法。

在介绍算法之前,我们先来看这样一道题目:

题目

题目链接:洛谷P3366

题目很容易理解,即求出给定图的最小生成树边权和。那么,我们来看看这些算法吧!

Kruskal

基本思路

Kruskal(克鲁斯卡尔) 是一种贪心策略,类似图论中的Bellman-Ford算法。

简单来说,如果我们挑选了 n − 1 n-1 n1 条较小边,那么显而易见,这 n − 1 n-1 n1 条边的权值相加也会是一个较小值。按照这种思路,我们可以挑选 n − 1 n-1 n1 G G G 里面最小的边并将它们相连。

但是,你以为这就完了?怎么可能。

显然我们上面的做法有一个缺陷:它虽然保证了边权和最小,但是得出的却并不一定是一棵树。相反,它反而有可能得出来一个图(或森林)。我们需要解决这个问题。

显然,对于一条 u v u \leftrightarrow v uv 的无向边,若点 u u u 和点 v v v 已经连通(直接或间接),那么我们就不再需要加入当前边了。

对于每次遍历,我们都会对当前边的所到达的节点进行一个排查:如果节点已经连通,则无需加边;否则连接两点。

这样一来,我们就剩下一个最重要的问题没解决了:如何判断两个点有没有连通?

我们可以使用并查集这个数据结构进行存储和判重。每次判断两点的连通性的时候,我们只需要查询他们的祖先是否相同即可。同理,对于每次连接操作,我们只需要进行并查集的合并操作来合并 u , v u,v u,v 两点即可。

参考代码
#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N = 2 * 1e5 + 10;

struct edge {
      // 存边
    int u, v, w;
} e[N];
edge mst[5010];  // 最小生成树
int vtx[5010], k, ans, n, m;  // vtx并查集数组,k当前最小生成树节点数,ans边权和

bool cmp(edge a, edge b) {
    
    return a.w < b.w;  // 按照边权排序
}

int Find(int x) {
      // 并查集查找操作
    if (vtx[x] == x) return x;
    return vtx[x] = Find(vtx[x]);
}

void Union(int u, int v) {
      // 并查集合并操作
    int fu = Find(u), fv = Find(v);
    if (fu != fv) vtx[fv] = fu;
}

void kruskal() {
      // Kruskal最小生成树
    for (int i = 0; i < m; i++) {
      // 遍历所有边
        if (Find(e[i].u) != Find(e[i].v)) {
      // 如果两点没有连接
            k++;  // MST边数++
            mst[k].u = e[i].u, mst[k].v = e[i].v, mst[k].w = e[i].w;  // 记录当前边
            ans += e[i].w;  // 总权重增加
            Union(e[i].u, e[i].v);  // 连接两点
        } 
    }
}

int main() {
    
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) vtx[i] = i;  // 并查集初始化,祖先都是自己,即每个点都未连接
    for (int i = 0; i < m; i++) scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
    sort(e, e + m, cmp);  // 对边权进行排序
    kruskal();  // 获取MST
    if (k == n - 1) printf("%d\n", ans);  // 如果边数满足条件,输出总权值
    else printf("orz");  // 否则输出orz
    return 0;
}

Prim

基本思路

Prim(普利姆) 是Dijkstra的一个扩展。

Prim算法与Dijkstra算法唯一的区别在于:Prim算法所记录的距离(dis数组)并非从某个起点到终点的距离,而是当前的生成树到某个点的最短距离。

其余部分与Dijkstra算法一致。同样,Prim算法也可以使用堆进行优化,以提高效率。

但是,一般我们不推荐在稀疏图上使用Prim堆优化。堆优化后的Prim在稀疏图上的效率与Kruskal类似,但明显Kruskal代码复杂度较低。

参考代码
#include <iostream>
#include <cstdio>
#include <memory.h>
#include <algorithm>
using namespace std;
const int N = 5010;

int g[N][N], dis[N];  // 邻接矩阵存图
bool vis[N];  // 是否经过了某个点
int n, m, ans, u, v, w;

void initialize() {
      // 初始化
    memset(g, 0x3f, sizeof(g));
    memset(dis, 0x3f, sizeof(dis));
}

void addEdge(int u, int v, int w) {
      // 添加一条 u <-> v,权值为w的无向边
    if (u != v && g[u][v] > w) g[u][v] = g[v][u] = w;
}

void prim() {
      // Prim
    for (int i = 0; i < n; i++) {
      // 遍历每个点
        int k = 0;  // 距离最近的点的坐标
        for (int j = 1; j <= n; j++) {
      // 寻找距离点i最近的未访问过的点
            if (!vis[j] && dis[j] < dis[k]) k = j;
        }
        vis[k] = 1;  // 标记访问
        ans += dis[k];  // 记录权值
        for (int j = 1; j <= n; j++) {
      // 再次遍历每个点,更新最短路
            if (!vis[j] && dis[j] > g[k][j]) {
      // 未访问过且路程比当前记录的小
                dis[j] = g[k][j];  // 更新权值
            }
        }
    }
}

int main() {
    
    initialize();
    scanf("%d%d", &n, &m);
    while (m--) {
    
        scanf("%d%d%d", &u, &v, &w);
        addEdge(u, v, w); addEdge(v, u, w);
    }
    dis[1] = 0;  // 约定俗成,从点1跑prim,赋值为0是为了消除自环
    prim();
    for (int i = 1; i <= n; i++) {
      // 判断是否建立成树
        if (!vis[i]) {
      // 如果有点未访问,则没有最小生成树
            printf("orz\n");
            return 0;
        }
    }
    printf("%d\n", ans);  // 否则输出权重和
    return 0;
}

参考资料

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_47212138/article/details/119891944

智能推荐

Rose双机热备两款软件原理介绍以及共享存储双机热备方案和镜像双机热备方案介绍_rose主备切换-程序员宅基地

文章浏览阅读5.1k次,点赞2次,收藏7次。一. RoseHA的工作原理  RoseHA双机系统的两台服务器(主机)都与磁盘阵列(共享存储)系统直接连接,用户的操作系统、应用软件和RoseHA高可用软件分别安装在两台主机上,数据库等共享数据存放在存储系统上,两台主机之间通过私用心跳网络连接。配置好的系统主机开始工作后,RoseHA软件开始监控系统,通过私用网络传递的心跳信息,每台主机上的RoseHA软件都可监控另一台主机的状态。当工作主机..._rose主备切换

Redis缓存数据库SaaS多租户实现方案-程序员宅基地

文章浏览阅读2.7k次。一、前言上2个章节已经实现了mysql和MongoDB的多租户切实现方案,本章将继续学习Redis的多数据源切换。Redis服务器默认有16个database,我们可以将每个租户的数据放到其中一个database中,也可以部署多台Redis服务器,每个租户使用一个Redis服务器,也可以把两者结合起来,Redis服务器部署多台,先在一台的16个Database上放,放满了16个Database然后再往下一台Redis服务器上放。这种方式需要有一个MySQL数据库表存储每台Redis服务器的Databa

win10触摸键盘TabTip软件特性-程序员宅基地

文章浏览阅读2.3k次。win10触摸键盘通过::SendMessage隐藏方式没有效果HWND hWnd = ::FindWindow(L"OSKMainClass", NULL);if ( hWnd ){::SendMessage(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, 0);} win10触摸键盘无法找到状态窗口状态,isWidowsVisible,GetWindowPlacement,GetWindowLong,状态没有变化 ..._tabtip

android实时声音信号波形_【每日一题】(八上)通过波形图比较声音的特性(学苑帮你成长一每日一题精析)9月25日...-程序员宅基地

文章浏览阅读1.5k次。9月25日 通过波形图比较声音的特性中考频度:★★☆☆☆难易程度:★☆☆☆☆(2019·广东初二期末)把频率为256 Hz音叉发出的声音信号输入示波器,示波器展现的波形如图甲所示。若把频率为512 Hz音叉发出的声音信号输入同一设备的示波器,其波形可能图中的__________。【参考答案】丁【试题解析】由题知,原来音叉发出声音的频率为256 Hz,现在音叉发出声音的频率为512 Hz..._规格为256hz音叉声音波形如图所示,将512hz音叉的声音输入同一设置的示波器后,其波

关于Win10安装SQLServer后在程序中不能访问的解决方法_msvcr120 sqlservr windows 无非是访问-程序员宅基地

文章浏览阅读8.1k次。前两天刚在win10中安装完SqlServer 2008 R2 ,安装步骤可以参见这篇文章,装完之后打开 Sql Management Studio,登陆、查询、用Navicat连接都没有问题,于是开始转移以前的项目到本机。然后就出现问题了,在访问项目的时候只要是关于查询数据库的程序全都卡半天,最后报个(org.hibernate.exception.GenericJDBCException: C..._msvcr120 sqlservr windows 无非是访问

Json文件格式化方法_json格式化-程序员宅基地

文章浏览阅读5.3w次,点赞2次,收藏16次。本文详细介绍了 JSON 文件格式化的方法。通过深入探讨,文中提供了多种有效的方式来对 JSON 文件进行格式化,以提高其可读性和可维护性。这些方法涵盖了使用特定工具或的相关技巧和要点。读者可以从中了解到如何快速、准确地对 JSON 文件进行格式化,以便更好地理解和处理其中的数据。_json格式化

随便推点

matplotlib绘图时横纵坐标和图例的字体大小如何设置_matplotlib绘图横纵坐标设置-程序员宅基地

文章浏览阅读8.8k次,点赞3次,收藏8次。横纵坐标字体大小调节:通过fontsize可以进行调节ax1.set_ylabel("AUC",fontsize=20)ax2.set_ylabel("Logloss",fontsize=20)图例字体大小调节:在plt.legend中加一个prop={"size":18,"weight":"black"}即可_matplotlib绘图横纵坐标设置

HTML5特效按钮_html5 特效按钮-程序员宅基地

文章浏览阅读1w次,点赞2次,收藏19次。作为前端开发者,我们肯定都使用过非常多的jQuery插件,毋庸置疑,jQuery非常流行,尤其是结合HTML5和CSS3以后,让这些jQuery插件有了更多地动画效果,更为绚丽多彩。下面分享了一些超炫酷的jQuery/HTML5应用,一起来看看。1、HTML5/CSS3一组可爱的3D按钮这是一款利用HTML5和CSS3制作而成的按钮组合,这款CSS按钮非常具有个性化。该CSS3按钮_html5 特效按钮

树莓派(Raspberry Pi 4)开启和连接蓝牙_树莓派连接蓝牙耳机并使用麦克风-程序员宅基地

文章浏览阅读1.8w次,点赞4次,收藏43次。参考连接: link.1、查看树莓派蓝牙开启状态_树莓派连接蓝牙耳机并使用麦克风

Python3输入输出与字符串格式化_%s.%d' %()-程序员宅基地

文章浏览阅读3.3k次,点赞3次,收藏12次。介绍了输入(input)、输出(print),及字符串格式化(F-string、format与%)方式_%s.%d' %()

EL表达式比较字符串或是数字格式的数值是否相等,为true,却不执行为true时的代码_el 表达式 判断字符串和数字相等-程序员宅基地

文章浏览阅读9.3k次。问题:EL表达式比较字符串或是数字格式的数值是否相等,为true,却不执行为true时的代码。示例:true原因:有可能是test="${ 1 == 1}(这里多个空格)",即大括号与双引号之间多了空格,这个时候,就不会打印true。去掉多余的空格就可以了_el 表达式 判断字符串和数字相等

GCC-3.4.6源代码学习笔记(26续1)_-feliminate-unused-debug-types-程序员宅基地

文章浏览阅读2k次。common_handle_option (continue) 909 case OPT_fcall_used_:910 fix_register (arg, 0, 1);911 break;912 913 case OPT_fcall_saved_:914 fix_register (arg, 0, 0)_-feliminate-unused-debug-types