图论- 最小生成树

一、最小生成树-prim算法 

1.1 最小生成树概念

一幅图可以有很多不同的生成树,比如下面这幅图,红色的边就组成了两棵不同的生成树:

对于加权图,每条边都有权重(用最小生成树算法的现实场景中,图的边权重一般代表成本、距离这样的标量),所以每棵生成树都有一个权重和。比如上图,右侧生成树的权重和显然比左侧生成树的权重和要小。

那么最小生成树很好理解了,所有可能的生成树(包含所有顶点)中,权重和最小的那棵生成树就叫「最小生成树」

1.2 稠密图-朴素prim

和djikstra很像

const int INF = 0x3f3f3f3f; // 定义一个非常大的数,用作无穷远的初始化值
int n; // n表示图中的顶点数
int g[N][N]; // 邻接矩阵,用于存储图中所有边的权重
int dist[N]; // 用于存储其他顶点到当前最小生成树的最小距离
bool st[N]; // 用于标记每个顶点是否已经被加入到最小生成树中// Prim算法的实现,返回最小生成树的总权重
int prim() {memset(dist, 0x3f, sizeof dist); // 初始化所有顶点到MST的距离为无穷远int res = 0; // 存储最小生成树的总权重for (int i = 0; i < n; i++) { // 主循环,每次添加一个顶点到MSTint t = -1; // 用于找到当前未加入MST且dist最小的顶点for (int j = 1; j <= n; j++) // 遍历所有顶点,找到tif (!st[j] && (t == -1 || dist[t] > dist[j]))t = j;//t就是当前加入最小生成树的顶点if (i && dist[t] == INF) return INF; // 如果图不连通,则返回INFif (i) res += dist[t]; // 非首次迭代时,累加到MST的距离st[t] = true; // 将顶点t加入到MST中//再从T出发,更新所有未加入顶点到T的距离,用于下一轮新的T的更新for (int j = 1; j <= n; j++) // 更新其他所有顶点到MST的最小距离if (!st[j]) dist[j] = min(dist[j], g[t][j]);}return res; // 返回最小生成树的总权重
}

例题: 

#include<cstring>
#include<iostream>
#include<algorithm>using namespace std;const int N = 510,M = 100010,INF = 0x3f3f3f3f;int n,m;
int g[N][N];
int dist[N];
bool used[N];int prim(){memset(dist,0x3f,sizeof dist);int res = 0;for(int i = 0;i < n;i++){int t = -1;for(int j = 1;j <= n; j++){if((!used[j]) && (t == -1 || dist[t] > dist[j]))t = j;}used[t] = true;//第一步的dist[t]为INFif(i && dist[t] == INF) return INF;if(i)res += dist[t];for(int j = 1;j <= n;j++){if(!used[j])dist[j] = min(dist[j],g[t][j]);}}return res;
}int main(){scanf("%d%d",&n,&m);//重要memset(g,0x3f,sizeof(g));for(int i = 0;i < m; i++){int u,v,w;scanf("%d%d%d",&u,&v,&w);g[u][v] = g[v][u] = min(g[u][v],w);}int r = prim();if(r == INF)puts("impossible");else printf("%d",r);return 0;
}

1.3 堆优化的prim-不常用,且复杂,一般用kruskal替代

省略 

二、最小生成树-kruskal算法

1.并查集复习

1.1 并查集(Union-Find)算法

是一个专门针对「动态连通性」的算法,我之前写过两次,因为这个算法的考察频率高,而且它也是最小生成树算法的前置知识

动态连通性

简单说,动态连通性其实可以抽象成给一幅图连线。比如下面这幅图,总共有 10 个节点,他们互不相连,分别用 0~9 标记:

这里所说的「连通」是一种等价关系,也就是说具有如下三个性质:

1、自反性:节点 p 和 p 是连通的。

2、对称性:如果节点 p 和 q 连通,那么 q 和 p 也连通。

3、传递性:如果节点 p 和 q 连通,q 和 r 连通,那么 p 和 r 也连通。

 现在我们的 Union-Find 算法主要需要实现这三个 API:

class UF {
public:/* 将 p 和 q 连接 */void union(int p, int q);/* 判断 p 和 q 是否连通 */bool connected(int p, int q);/* 返回图中有多少个连通分量 */int count();
};

函数功能说明:

比如说之前那幅图,0~9 任意两个不同的点都不连通,调用 connected 都会返回 false,连通分量为 10 个。

如果现在调用 union(0, 1),那么 0 和 1 被连通,连通分量降为 9 个。

再调用 union(1, 2),这时 0,1,2 都被连通,调用 connected(0, 2) 也会返回 true,连通分量变为 8 个。

 初始化:

怎么用森林来表示连通性呢?我们设定树的每个节点有一个指针指向其父节点,如果是根节点的话,这个指针指向自己。比如说刚才那幅 10 个节点的图,一开始的时候没有相互连通,就是这样:

代码如下:

class UF {// 记录连通分量private:int count;// 节点 x 的父节点是 parent[x]int* parent;public:/* 构造函数,n 为图的节点总数 */UF(int n) {// 一开始互不连通this->count = n;// 父节点指针初始指向自己parent = new int[n];for (int i = 0; i < n; i++)parent[i] = i;}/* 其他函数 */
};

 union实现:

操作如下:

代码如下:

class UF {// 为了节约篇幅,省略上文给出的代码部分...public:void union(int p, int q) {int rootP = find(p);int rootQ = find(q);if (rootP == rootQ)return;// 将两棵树合并为一棵parent[rootP] = rootQ;// parent[rootQ] = rootP 也一样count--; // 两个分量合二为一}/* 返回某个节点 x 的根节点 */int find(int x) {// 根节点的 parent[x] == xwhile (parent[x] != x)x = parent[x];return x;}/* 返回当前的连通分量个数 */int count() {return count;}
};

 connected实现:

代码如下:

class UF {
private:// 省略上文给出的代码部分...public:bool connected(int p, int q) {int rootP = find(p);int rootQ = find(q);return rootP == rootQ;}
};

 1.2 平衡性优化-union优化

分析union和connected的时间复杂度,我们发现,主要 API connected和 union 中的复杂度都是 find 函数造成的,所以说它们的复杂度和 find 一样。

find 主要功能就是从某个节点向上遍历到树根,其时间复杂度就是树的高度。我们可能习惯性地认为树的高度就是 logN,但这并不一定。logN 的高度只存在于平衡二叉树,对于一般的树可能出现极端不平衡的情况,使得「树」几乎退化成「链表」,树的高度最坏情况下可能变成 N

图论解决的都是诸如社交网络这样数据规模巨大的问题,对于 union 和 connected 的调用非常频繁,每次调用需要线性时间完全不可忍受。

关键在于 union 过程,我们一开始就是简单粗暴的把 p 所在的树接到 q 所在的树的根节点下面,那么这里就可能出现「头重脚轻」的不平衡状况,比如下面这种局面:

 长此以往,树可能生长得很不平衡。我们其实是希望,小一些的树接到大一些的树下面,这样就能避免头重脚轻,更平衡一些。解决方法是额外使用一个 size 数组,记录每棵树包含的节点数,我们不妨称为「重量」:

class UF {
private:int count;int* parent;// 新增一个数组记录树的“重量”int* size;public:UF(int n) {this->count = n;parent = new int[n];// 最初每棵树只有一个节点// 重量应该初始化 1size = new int[n];for (int i = 0; i < n; i++) {parent[i] = i;size[i] = 1;}}/* 其他函数 */
};

比如说 size[3] = 5 表示,以节点 3 为根的那棵树,总共有 5 个节点。这样我们可以修改一下 union 方法:

class UF {
private:// 为了节约篇幅,省略上文给出的代码部分...
public:void union(int p, int q) {int rootP = find(p);int rootQ = find(q);if (rootP == rootQ)return;// 小树接到大树下面,较平衡if (size[rootP] > size[rootQ]) {parent[rootQ] = rootP;size[rootP] += size[rootQ];} else {parent[rootP] = rootQ;size[rootQ] += size[rootP];}count--;}
};

这样,通过比较树的重量,就可以保证树的生长相对平衡,树的高度大致在 logN 这个数量级,极大提升执行效率。

此时,find , union , connected 的时间复杂度都下降为 O(logN),即便数据规模上亿,所需时间也非常少。

1.3 路径压缩-find优化

其实我们并不在乎每棵树的结构长什么样,只在乎根节点

因为无论树长啥样,树上的每个节点的根节点都是相同的,所以能不能进一步压缩每棵树的高度,使树高始终保持为常数?

这样每个节点的父节点就是整棵树的根节点,find 就能以 O(1) 的时间找到某一节点的根节点,相应的,connected 和 union 复杂度都下降为 O(1)。

要做到这一点主要是修改 find 函数逻辑,非常简单,但你可能会看到两种不同的写法。

方法1:

class UF {// 为了节约篇幅,省略上文给出的代码部分...private:int find(int x) {while (parent[x] != x) {// 这行代码进行路径压缩parent[x] = parent[parent[x]];x = parent[x];}return x;}
};

每次使得当前x指向父节点的父节点,这样会将一些节点向上移,然后缩短树的长度

压缩结束为: 

方法二:

class UF {// 为了节约篇幅,省略上文给出的代码部分...// 第二种路径压缩的 find 方法public:int find(int x) {if (parent[x] != x) {parent[x] = find(parent[x]);}return parent[x];}
};

 其迭代写法如下(便于理解):

int find(int x) {// 先找到根节点int root = x;while (parent[root] != root) {root = parent[root];}// 然后把 x 到根节点之间的所有节点直接接到根节点下面int old_parent = parent[x];while (x != root) {parent[x] = root;x = old_parent;old_parent = parent[old_parent];}return root;
}

最终效果: 

1.4 并查集框架-优化后 

class UF {
private:// 连通分量个数int count;// 存储每个节点的父节点int *parent;public:// n 为图中节点的个数UF(int n) {this->count = n;parent = new int[n];for (int i = 0; i < n; i++) {parent[i] = i;}}// 将节点 p 和节点 q 连通void union_(int p, int q) {int rootP = find(p);int rootQ = find(q);if (rootP == rootQ)return;parent[rootQ] = rootP;// 两个连通分量合并成一个连通分量count--;}// 判断节点 p 和节点 q 是否连通bool connected(int p, int q) {int rootP = find(p);int rootQ = find(q);return rootP == rootQ;}int find(int x) {if (parent[x] != x) {parent[x] = find(parent[x]);}return parent[x];}// 返回图中的连通分量个数int count_() {return count;}
};

2.kruskal

给你输入编号从 0 到 n - 1 的 n 个结点,和一个无向边列表 edges(每条边用节点二元组表示),请你判断输入的这些边组成的结构是否是一棵树。

如果输入:

n = 5
edges = [[0,1], [0,2], [0,3], [1,4]]

 这些边构成的是一棵树,算法应该返回 true:

 输入:

n = 5
edges = [[0,1],[1,2],[2,3],[1,3],[1,4]]

 形成的就不是树结构了,因为包含环:

我们思考为何会产生环?仔细体会下面两种添边的差别

 

总结一下规律就是:

对于添加的这条边,如果该边的两个节点本来就在同一连通分量里,那么添加这条边会产生环;反之,如果该边的两个节点不在同一连通分量里,则添加这条边不会产生环

那么只需要在union两节点之前先检测两节点是否已经connection,如果已连接所有添加后会生成环,则返回false。同时需要注意count==1,不然就是森林了。

代码如下:

class UF {
public:vector<int> parent;UF(int n) {for (int i = 0; i < n; i++) {parent.push_back(i);}}int find(int x) {while (x != parent[x]) {parent[x] = parent[parent[x]];x = parent[x];}return x;}void union_(int p, int q) {int root_p = find(p);int root_q = find(q);parent[root_p] = root_q;}bool connected(int p, int q) {int root_p = find(p);int root_q = find(q);return root_p == root_q;}int count() {int cnt = 0;for (int i = 0; i < parent.size(); i++) {if (parent[i] == i) {cnt++;}}return cnt;}
};bool validTree(int n, vector<vector<int>>& edges) {UF uf(n);// 遍历所有边,将组成边的两个节点进行连接for (auto edge : edges) {int u = edge[0];int v = edge[1];// 若两个节点已经在同一连通分量中,会产生环if (uf.connected(u, v)) {return false;}// 这条边不会产生环,可以是树的一部分uf.union_(u, v);}// 要保证最后只形成了一棵树,即只有一个连通分量return uf.count() == 1;
}

3.连接所有点的最小费用-kruskal算法

 所谓最小生成树,就是图中若干边的集合(我们后文称这个集合为 mst,最小生成树的英文缩写),你要保证这些边:

1、包含图中的所有节点。

2、形成的结构是树结构(即不存在环)。

3、权重和最小。

有之前题目的铺垫,前两条其实可以很容易地利用 Union-Find 算法做到,关键在于第 3 点,如何保证得到的这棵生成树是权重和最小的。

这里就用到了贪心思路:

将所有边按照权重从小到大排序,从权重最小的边开始遍历,如果这条边和 mst 中的其它边不会形成环,则这条边是最小生成树的一部分,将它加入 mst 集合;否则,这条边不是最小生成树的一部分,不要把它加入 mst 集合

以此题为例:

 此题虽然是使用kruskal算法,但是并不是直接套用,还要有一些值得注意的事项

1:我们要将题目中的给出点,转换为点组合并且将权重添加进去

在题中只给出一个点的坐标,我们需要想方法转换为两个点的链接,所以需要将每个点(两个坐标组合)转换为一个符号标记,在链接数组把相连的两个符号放一起就行了,很明显,我们使用0-n-1来记录每一个点是最合适的,不仅方便遍历也一目了然

因此有如下代码:

        vector<vector<int>> edges;for(int i =0;i < points.size();i++){//此处不能写为int j = 0,这样会重复导致超时,根据求子集的思想,应该从j=i+1开始for(int j = i+1;j < points.size();j++){// if(i == j)continue;//因为j=i+1开始,所有不需要这句判断int w1 = abs(points[i][0]-points[j][0]);int w2 = abs(points[i][1]-points[j][1]);edges.push_back({i,j,w1+w2});}}

 2:我们要对得到的数组进行排序,而且是对权重维度排序,这就需要我们利用lambda来自定义sort的排序方式了

有代码如下:

        sort(edges.begin(),edges.end(),[](const vector<int>& a,const vector<int>& b){return a[2] < b[2];});

依照kruskal算法,可以写出如下完整代码:

class uf{private:int count;vector<int> parent;public:uf(int n){this->count = n;// parent = new int[n];parent.resize(n);for(int i=0;i < n;i++){parent[i] = i;}}int find(int x){if(parent[x]!=x)parent[x] = find(parent[x]);return  parent[x];}void Union(int p,int q){int rootp = find(p);int rootq = find(q);if(rootp == rootq)return;parent[rootp] = rootq;count--;}bool connection(int p,int q){int rootp = find(p);int rootq = find(q);return rootp == rootq;}
};class Solution {
public:int minCostConnectPoints(vector<vector<int>>& points) {vector<vector<int>> edges;for(int i =0;i < points.size();i++){//此处不能写为int j = 0,这样会重复导致超时,根据求子集的思想,应该从j=i+1开始for(int j = i+1;j < points.size();j++){// if(i == j)continue;//因为j=i+1开始,所有不需要这句判断int w1 = abs(points[i][0]-points[j][0]);int w2 = abs(points[i][1]-points[j][1]);edges.push_back({i,j,w1+w2});}}sort(edges.begin(),edges.end(),[](const vector<int>& a,const vector<int>& b){return a[2] < b[2];});uf uf(points.size());int sum_w = 0;for(auto& s : edges){int q = s[0],p = s[1],w = s[2];if(uf.connection(p,q))continue;sum_w +=w;uf.Union(p,q);}return sum_w;}
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/780268.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

UI的设计

一、RGB888的显示 即红色&#xff0c;绿色&#xff0c;蓝色都为8位&#xff0c;即通常说的24位色。可以很好显示各种过渡颜色。从硬件上&#xff0c;R、G、B三基色的连接线各需要有8根&#xff0c;即24根数据线&#xff1b;软件上存储的数据量也需要24位&#xff0c;即3个字节&…

主干网络篇 | YOLOv8更换主干网络之EfficientNet

前言:Hello大家好,我是小哥谈。EfficientNet是一种高效的卷积神经网络架构,由Mingxing Tan和Quoc V. Le在2019年提出,其设计思想是在不增加计算复杂度的情况下提高模型的准确性。它引入了一个称为"复合系数"的概念,该系数用于同时缩放网络的深度、宽度和分辨率。…

RabbitMQ 延时消息实现

1. 实现方式 1. 设置队列过期时间&#xff1a;延迟队列消息过期 死信队列&#xff0c;所有消息过期时间一致 2. 设置消息的过期时间&#xff1a;此种方式下有缺陷&#xff0c;MQ只会判断队列第一条消息是否过期&#xff0c;会导致消息的阻塞需要额外安装 rabbitmq_delayed_me…

光伏发电量预测(Python代码,CNN结合LSTM,TensorFlow框架)

1.数据集&#xff08;开始位置&#xff09;&#xff0c;数据集免费下载链接&#xff1a;https://download.csdn.net/download/qq_40840797/89051099 数据集一共8列&#xff0c;第一列是时间&#xff0c;特征列一共有6列&#xff1a;"WindSpeed" - 风速 "Sunshi…

C++template之类模版进一步了解

前言&#xff1a;这一篇是在我的上一篇文章的基础上&#xff0c;再进一步所写的。 链接&#xff1a;CTemplate&#xff1c;&#xff1e;模版的介绍及深度解析-CSDN博客 一、类模板实例化 1.非类型模版参数 类型模版参数&#xff1a;就是跟在 class后面或者typename后的类型 非…

visual studio报:引发的异常:“System.DllNotFoundException”(位于 ConsoleCAN1.exe 中)

最近在重构CAN通信的代码&#xff0c;把论文中的java转为C#实现&#xff0c;由于某种原因&#xff0c;java不能复现&#xff0c;所以转为c#。 然而c#的重构过程遇到许多问题&#xff0c;因为两种语言的编程方式、线程等等实现上有所差异。 其中一个错误&#xff1a; 引发的异…

vue3+threejs新手从零开发卡牌游戏(二十一):添加战斗与生命值关联逻辑

首先将双方玩家的HP存入store中&#xff0c;stores/common.ts代码如下&#xff1a; import { ref, computed } from vue import { defineStore } from piniaexport const useCommonStore defineStore(common, () > {const _font ref() // 字体const p1HP ref(4000) // 己…

三菱Q系列PLC以太网TCP通讯FB块源码

三菱Q系列PLC的tcp通讯&#xff0c;客户端和服务器两个变量好用的FB块&#xff0c;调用块就可以实现通讯连接&#xff0c;不需要自己写程序&#xff0c;简单配置引脚就可以。该块还集成了断网&#xff0c;连接错误&#xff0c;发送接收数据错误报警等功能。具体功能见下面介绍.…

sql Tuning Advisor启用导致业务性能问题

数据库每天晚上10点后业务性能很卡&#xff0c;大量的insert被堵塞&#xff0c;查询等待事件发现有大量的“library cache lock”和“cursor: pin S wait on X”。 22:00数据库的统计信息开始收集&#xff0c; Sql Tuning Advisor堵塞了统计信息的收集&#xff0c;等待事件是“…

利用HIVE的窗口函数进行SQL查询中出现的问题记录

student_info部分数据 score_info部分数据 course_info 1、问题复现 --完整SQL selectsti.stu_id,sti.stu_name,concat_ws(",",collect_set(ci.course_name)) over(partition by sti.stu_id) fromstudent_info sti left joinscore_info sci onsti.stu_idsci.stu_id l…

mysql之MyBatis核心工作原理

MyBatis核心工作原理 一、源码环境 1.手动编译源码 工欲善其事必先利其器。为了方便我们在看源码的过程中能够方便的添加注释&#xff0c;我们可以自己来从官网下载源码编译生成对应的Jar包&#xff0c;然后上传到本地maven仓库&#xff0c;再引用这个Jar。大家可以自行去官…

matlab 点云可视化(6)——点云按强度进行可视化

目录 一、功能概述1、算法概述2、主要函数二、代码示例三、结果展示四、参考链接本文由CSDN点云侠原创原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫。 一、功能概述 1、算法概述 点云按强度进行可视化 2、主要函数

绿联 安装Uptime Kuma - 一款开源的服务器监控和状态检测工具

Uptime Kuma 功能简介 Uptime Kuma 是一款开源的服务器监控和状态检测工具&#xff0c;它帮助您跟踪服务器的可用性、性能和健康状态。 主要功能&#xff1a; 服务器监控 Uptime Kuma 可以监控多个服务器&#xff0c;包括 Web 服务器、数据库服务器、应用程序服务器等。 它会定…

Antd Vue3 使用 Anchor 锚点组件记录

项目场景 客户要求做一个表单页面&#xff0c;表单数据分为三步&#xff0c;每一步骤是一个单独的 Vue 组件&#xff0c;表单上方需要使用锚点组件实现锚点定位到每一步的功能。 代码总览 <template><div class"guided-form-content-wrapper"><!-- …

初步了解C++

目录 一&#xff1a;什么是C&#xff1f; 二.C发展史 三:C关键字 四&#xff1a;命名空间 4.1命名空间的介绍 4.2命名空间的使用 4.3命名空间的使用 4.3.1使用作用域限定符 4.3.2 使用using将命名空间的某个成员引入 4.3.3使用using把整个命名空间展开 4.4命名空…

每天学习一个Linux命令之scp

每天学习一个Linux命令之scp 在Linux系统中&#xff0c;scp&#xff08;Secure Copy&#xff09;是一个用于在本地主机和远程主机之间进行文件传输的命令行工具。它基于SSH协议&#xff0c;通过加密方式传输文件&#xff0c;确保传输的安全性和完整性。scp命令非常强大且使用简…

【蓝桥杯选拔赛真题50】C++简易炸弹超人 第十四届蓝桥杯青少年创意编程大赛 算法思维 C++编程选拔赛真题解析

目录 C简易炸弹超人 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 七、推荐资料 C简易炸弹超人 第十四届蓝桥杯青少年创意编程大赛C选拔赛真题 一、题目要求 1、编程实现 有一块矩形游戏场地&#x…

聊聊k8s服务发现的优缺点

序 本文主要研究一下使用k8s服务发现的优缺点 spring cloud vs kubernetes 这里有张spring cloud与kubernetes的对比&#xff0c;如果将微服务部署到kubernetes上面&#xff0c;二者有不少功能是重复的&#xff0c;可否精简。 这里主要是讲述一下如果不使用独立的服务发现&am…

SpringMVC注解及使用规则

文章目录 前言一、SpringMVC注解是什么&#xff1f;二、使用步骤1.注解使用2创建JSP3 SpringMVC视图1. 逻辑视图&#xff08;Logical View&#xff09;2. 物理视图&#xff08;Physical View&#xff09;区别和关系 4 SpringMVC注解总结 总结 前言 提示&#xff1a;这里可以添…

建立一个简单的网页音乐盒模型效果#css#h5

“音乐盒”可以看做一个大盒子&#xff0c;用<div>标签进行定义。大盒子的上面为文本内容&#xff0c;可以在<div>标签中嵌套<h2>和<p>标签来实现&#xff1b;大盒子下面为图像&#xff0c;通过在<div>标签中嵌套<img/>标签来实现。 样式…