最近刚学了最小生成树,于是想趁热打火,先来总结一下~
本文章所有代码均以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 必须具备以下条件:
假设我们有如下的连通图 G G G(左),那么它的最小生成树 T T T 如图所示(右)。
显然对于图 G G G, T T T 的边权和在所有生成树中最小。我们称这样的树为图 G G G 的 最小生成树(简称MST)。
我们不妨来介绍两种较为常见的求最小生成树的算法。
在介绍算法之前,我们先来看这样一道题目:
题目链接:洛谷P3366。
题目很容易理解,即求出给定图的最小生成树边权和。那么,我们来看看这些算法吧!
Kruskal(克鲁斯卡尔) 是一种贪心策略,类似图论中的Bellman-Ford算法。
简单来说,如果我们挑选了 n − 1 n-1 n−1 条较小边,那么显而易见,这 n − 1 n-1 n−1 条边的权值相加也会是一个较小值。按照这种思路,我们可以挑选 n − 1 n-1 n−1 条 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(普利姆) 是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;
}
文章浏览阅读5.1k次,点赞2次,收藏7次。一. RoseHA的工作原理 RoseHA双机系统的两台服务器(主机)都与磁盘阵列(共享存储)系统直接连接,用户的操作系统、应用软件和RoseHA高可用软件分别安装在两台主机上,数据库等共享数据存放在存储系统上,两台主机之间通过私用心跳网络连接。配置好的系统主机开始工作后,RoseHA软件开始监控系统,通过私用网络传递的心跳信息,每台主机上的RoseHA软件都可监控另一台主机的状态。当工作主机..._rose主备切换
文章浏览阅读2.7k次。一、前言上2个章节已经实现了mysql和MongoDB的多租户切实现方案,本章将继续学习Redis的多数据源切换。Redis服务器默认有16个database,我们可以将每个租户的数据放到其中一个database中,也可以部署多台Redis服务器,每个租户使用一个Redis服务器,也可以把两者结合起来,Redis服务器部署多台,先在一台的16个Database上放,放满了16个Database然后再往下一台Redis服务器上放。这种方式需要有一个MySQL数据库表存储每台Redis服务器的Databa
文章浏览阅读2.3k次。win10触摸键盘通过::SendMessage隐藏方式没有效果HWND hWnd = ::FindWindow(L"OSKMainClass", NULL);if ( hWnd ){::SendMessage(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, 0);} win10触摸键盘无法找到状态窗口状态,isWidowsVisible,GetWindowPlacement,GetWindowLong,状态没有变化 ..._tabtip
文章浏览阅读1.5k次。9月25日 通过波形图比较声音的特性中考频度:★★☆☆☆难易程度:★☆☆☆☆(2019·广东初二期末)把频率为256 Hz音叉发出的声音信号输入示波器,示波器展现的波形如图甲所示。若把频率为512 Hz音叉发出的声音信号输入同一设备的示波器,其波形可能图中的__________。【参考答案】丁【试题解析】由题知,原来音叉发出声音的频率为256 Hz,现在音叉发出声音的频率为512 Hz..._规格为256hz音叉声音波形如图所示,将512hz音叉的声音输入同一设置的示波器后,其波
文章浏览阅读8.1k次。前两天刚在win10中安装完SqlServer 2008 R2 ,安装步骤可以参见这篇文章,装完之后打开 Sql Management Studio,登陆、查询、用Navicat连接都没有问题,于是开始转移以前的项目到本机。然后就出现问题了,在访问项目的时候只要是关于查询数据库的程序全都卡半天,最后报个(org.hibernate.exception.GenericJDBCException: C..._msvcr120 sqlservr windows 无非是访问
文章浏览阅读5.3w次,点赞2次,收藏16次。本文详细介绍了 JSON 文件格式化的方法。通过深入探讨,文中提供了多种有效的方式来对 JSON 文件进行格式化,以提高其可读性和可维护性。这些方法涵盖了使用特定工具或的相关技巧和要点。读者可以从中了解到如何快速、准确地对 JSON 文件进行格式化,以便更好地理解和处理其中的数据。_json格式化
文章浏览阅读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绘图横纵坐标设置
文章浏览阅读1w次,点赞2次,收藏19次。作为前端开发者,我们肯定都使用过非常多的jQuery插件,毋庸置疑,jQuery非常流行,尤其是结合HTML5和CSS3以后,让这些jQuery插件有了更多地动画效果,更为绚丽多彩。下面分享了一些超炫酷的jQuery/HTML5应用,一起来看看。1、HTML5/CSS3一组可爱的3D按钮这是一款利用HTML5和CSS3制作而成的按钮组合,这款CSS按钮非常具有个性化。该CSS3按钮_html5 特效按钮
文章浏览阅读1.8w次,点赞4次,收藏43次。参考连接: link.1、查看树莓派蓝牙开启状态_树莓派连接蓝牙耳机并使用麦克风
文章浏览阅读3.3k次,点赞3次,收藏12次。介绍了输入(input)、输出(print),及字符串格式化(F-string、format与%)方式_%s.%d' %()
文章浏览阅读9.3k次。问题:EL表达式比较字符串或是数字格式的数值是否相等,为true,却不执行为true时的代码。示例:true原因:有可能是test="${ 1 == 1}(这里多个空格)",即大括号与双引号之间多了空格,这个时候,就不会打印true。去掉多余的空格就可以了_el 表达式 判断字符串和数字相等
文章浏览阅读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