数据结构 —— 最小生成树

数据结构 —— 最小生成树

  • 什么是最小生成树
  • Kruskal算法
  • Prim算法

今天我们来看一下最小生成树

我们之前学习的遍历算法并没有考虑权值,仅仅就是遍历结点:
在这里插入图片描述今天的最小生成树要满足几个条件:

  1. 考虑权值
  2. 所有结点联通
  3. 权值之和最小
  4. 无环

在这里插入图片描述

什么是最小生成树

最小生成树(Minimum Spanning Tree,简称MST)是指在一个加权的、无向的连通图中,由所有顶点构成的一个子图,这个子图是一棵树,并且其所有边的权重之和最小。换句话说,最小生成树是在保证图中所有顶点连通的前提下,使得连接这些顶点的边的总成本最低的一棵树

最小生成树具有以下特性:

  1. 它包含图中的所有顶点。
  2. 它是一个没有环的连通子图(即树)。
  3. 它的边数比顶点数少一(对于 n 个顶点的图,有 n-1 条边)。
  4. 它的边的总权重是所有可能生成树中最小的。

最小生成树在很多实际应用中都有重要作用,例如在设计电信网络时,为了连接多个地点而需要铺设电缆或光纤,最小生成树可以用来确定一种成本最低的铺设方案。

求解最小生成树的常用算法包括:

  • Kruskal算法:此算法通过不断选择权重最小的边来构建最小生成树,同时避免添加会导致环路形成的边。它通常利用并查集(Disjoint Set Union)数据结构来检测环路。
  • Prim算法:此算法从任意一个顶点开始,逐步将顶点及其权重最小的连接边加入到生成树中,直到所有顶点都被包含进来。Prim算法可以使用优先队列(Priority Queue)来高效地选择下一个应加入的边。

我们今天就来介绍一下这两种算法:

Kruskal算法

Kruskal算法,简单来说,就是把所有边拿出来,从小到大挑边,构成最小生成树

Kruskal算法是一种用于寻找加权、无向连通图的最小生成树(Minimum Spanning Tree, MST)的贪心算法。它的核心思想是在不形成任何环路的情况下,选择权重最小的边来构建生成树,直到所有的顶点都被包含在树中。

以下是Kruskal算法的主要步骤:

  1. 排序边:将图中所有的边按照权重从小到大排序。
  2. 初始化森林:创建一个森林,其中每个顶点都是一个单独的树(即每个顶点都是一个独立的连通分量)。
  3. 选择边:遍历排序后的边列表。对于每条边,检查它的两个端点是否已经在同一棵树中(即是否属于同一个连通分量)。如果不是,将这条边添加到最小生成树中,并将这两个顶点所在的树合并成一棵更大的树。
  4. 重复步骤3:继续选择满足条件的边,直到最小生成树中包含了图中的所有顶点,或者已经选择了n-1条边(其中n是顶点的数量)。

在这里插入图片描述

Kruskal算法的关键在于能够快速地检测边的两个端点是否属于同一棵树,这通常是通过使用并查集(Union-Find)数据结构来实现的。并查集允许我们在对数时间内执行“查找”操作(确定顶点所属的树)和“合并”操作(将两棵树合并成一棵树)。

// 使用Kruskal算法计算最小生成树的总权重
W Kruskal(Self& minTree) // Self应为当前类的引用,minTree是用于存储最小生成树的实例
{
    // 初始化最小生成树的顶点集和索引
    minTree._vertex = _vertex;
    minTree._index = _index;
    minTree._matrix.resize(_vertex.size()); // 创建一个邻接矩阵,用于存储最小生成树中的边的权重

    for (auto& e : minTree._matrix) // 将邻接矩阵的所有元素初始化为最大权重值MAX_W
    {
        e.resize(_vertex.size(), MAX_W);
    }

    // 创建一个优先级队列,用于存储边的信息
    priority_queue<Edge, vector<Edge>, greater<Edge>> pq;

    // 将所有边(除了自环和重复边)加入优先级队列
    for (size_t i = 0; i < _vertex.size(); i++) 
    {
        for (size_t j = 0; j < _vertex.size(); j++) 
        {
            if (i < j && _matrix[i][j] != MAX_W) // 确保不加入自环和重复边
            {
                pq.push(Edge(i, j, _matrix[i][j])); // 将边加入优先级队列
            }
        }
    }

    // 初始化变量,用于记录最小生成树的总权重和边的数量
    W total = W();
    int size = 0;

    UnionFindSet ufs(_vertex.size()); // 创建并查集,用于判断顶点是否已经连接

    while (!pq.empty()) // 当优先级队列非空时
    {
        Edge min = pq.top(); // 取出权重最小的边
        pq.pop(); // 移除已取出的边

        // 判断边的两个顶点是否已经在同一集合内(即是否已经连接)
        if (!ufs.InSet(min._srci, min._desi)) 
        {
            cout << _vertex[min._srci] << "-" << _vertex[min._desi] << ":" << _matrix[min._srci][min._desi] << endl; // 打印边的信息

            minTree._AddEdge(min._srci, min._desi, min._w); // 将边加入最小生成树
            total += min._w; // 更新最小生成树的总权重

            ufs.Union(min._srci, min._desi); // 合并两个顶点所在的集合
            ++size; // 增加边的数量
        }
    }

    cout << endl;

    minTree.Print(); // 打印最小生成树

    // 如果边的数量等于顶点数量减一,则返回最小生成树的总权重
    if (size == _vertex.size() - 1)
    {
        return total;
    }
    else
    {
        return W(); // 否则返回默认权重值(可能表示无法形成最小生成树)
    }
}

我们可以来测试一下:

	void TestGraph2()
	{
		string a[] = {"海皇","高斯","小傲","小潮","胖迪","小杨","皖皖"};
		Graph<string, int,INT_MAX, false> g1(a, sizeof(a)/sizeof(a[0]));
		g1.AddEdge("小潮", "小傲", 30);
		g1.AddEdge("小潮", "高斯", 83);
		g1.AddEdge("小潮", "海皇", 34);
		g1.AddEdge("胖迪", "海皇", 78);
		g1.AddEdge("胖迪", "小傲", 76);
		g1.AddEdge("小杨", "皖皖", 54);
		g1.AddEdge("小杨", "高斯", 48);

		g1.Print();
		cout << endl;

		Graph<string, int, INT_MAX, false> kminTree;
		cout << "Kruskal:" << g1.Kruskal(kminTree) << endl;
	}

在这里插入图片描述按照Kruskal算法,构建出来的图是这样的:
在这里插入图片描述胖迪和海皇的关系被抹除了,其实我们之前的图里有环:
在这里插入图片描述

Kruskal算法的时间复杂度主要取决于排序边的操作和并查集的效率。在最好的情况下,排序边的时间复杂度为O(E log E),其中E是边的数量;并查集操作的时间复杂度接近常数,因此整个算法的时间复杂度近似为O(E log E)。由于排序的主导作用,该算法适用于边的数量远小于顶点数量平方的图,即稀疏图。

Prim算法

Prim算法和上面的思想差不多,但是,Prim算法会从一个顶点开始,这里我假设是从"小潮"开始:
在这里插入图片描述
跟小潮连接的3条边,会进入优先级队列,维护起来:
在这里插入图片描述接下来,会选择30的权重来构造,然后30这条边的另一边的小傲的边入优先级队列:
在这里插入图片描述
以此类推:

// 使用Prim算法构建并返回最小生成树的总权重
W Prim(Self& minTree, const V& vertex) // Self应该是当前类的引用,minTree是用于存储最小生成树的实例,vertex是顶点的容器
{
    // 初始化最小生成树的顶点集和索引
    minTree._vertex = _vertex;
    minTree._index = _index;
    minTree._matrix.resize(_vertex.size()); // 创建一个邻接矩阵,用于存储最小生成树中的边的权重

    // 初始化邻接矩阵的所有元素为最大权重值MAX_W
    for (auto& e : minTree._matrix)
    {
        e.resize(_vertex.size(), MAX_W);
    }

    // 区分顶点集合:已选择和未选择
    size_t srcIndex = FindSrci(vertex); // 找到起始顶点的索引
    vector<bool> select(_vertex.size(), false); // 已选择顶点集合,初始时所有顶点都未选择
    vector<bool> non_select(_vertex.size(), true); // 未选择顶点集合,初始时所有顶点都未被选择

    select[srcIndex] = true; // 起始顶点被标记为已选择
    non_select[srcIndex] = false; // 起始顶点从未选择集合中移除

    // 创建一个优先级队列,用于存储待处理的边
    priority_queue<Edge, vector<Edge>, greater<Edge>> pq; // 边按权重从小到大排序

    // 将起始顶点的邻接边加入优先级队列
    for (int i = 0; i < _vertex.size(); i++)
    {
        if (_matrix[srcIndex][i] != MAX_W) // 如果存在边,且不是最大权重(表示边存在)
        {
            pq.push(Edge(srcIndex, i, _matrix[srcIndex][i])); // 加入边信息到优先级队列
        }
    }

    // 初始化计数器和总权重
    size_t size = 0;
    W total = W(); // 初始化总权重为0

    // 当优先级队列非空时
    while (!pq.empty())
    {
        Edge min = pq.top(); // 获取当前权重最小的边
        pq.pop(); // 从队列中移除已处理的边

        // 如果目标顶点已被选择,跳过这条边
        if (select[min._desi]) continue;

        // 输出边的信息
        cout << _vertex[min._srci] << "-" << _vertex[min._desi] << ":" << _matrix[min._srci][min._desi] << endl;

        // 添加边到最小生成树
        minTree._AddEdge(min._srci, min._desi, min._w);

        // 标记目标顶点为已选择
        select[min._desi] = true;
        non_select[min._desi] = false;
        ++size; // 已处理的边数量加1
        total += min._w; // 更新总权重

        // 将新加入顶点的邻接边加入优先级队列
        for (size_t i = 0; i < _vertex.size(); i++)
        {
            if (_matrix[min._desi][i] != MAX_W && non_select[i]) // 如果存在边且目标顶点未被选择
            {
                pq.push(Edge(min._desi, i, _matrix[min._desi][i])); // 加入边信息到优先级队列
            }
        }
    }

    // 打印最小生成树
    minTree.Print();

    // 如果边的数量等于顶点数量减一,则返回最小生成树的总权重
    if (size == _vertex.size() - 1)
    {
        return total;
    }
    else
    {
        return W(); // 否则返回默认权重值(可能表示无法形成最小生成树)
    }
}
	void TestGraph2()
	{
		string a[] = {"海皇","高斯","小傲","小潮","胖迪","小杨","皖皖"};
		Graph<string, int,INT_MAX, false> g1(a, sizeof(a)/sizeof(a[0]));
		g1.AddEdge("小潮", "小傲", 30);
		g1.AddEdge("小潮", "高斯", 83);
		g1.AddEdge("小潮", "海皇", 34);
		g1.AddEdge("胖迪", "海皇", 78);
		g1.AddEdge("胖迪", "小傲", 76);
		g1.AddEdge("小杨", "皖皖", 54);
		g1.AddEdge("小杨", "高斯", 48);

		g1.Print();
		cout << endl;

		Graph<string, int, INT_MAX, false> kminTree;
		cout << "Kruskal:" << g1.Kruskal(kminTree) << endl;

		cout << endl;

		Graph<string, int, INT_MAX, false> pminTree;
		cout << "Prim:" << g1.Prim(pminTree,"小潮") << endl;

	}

在这里插入图片描述

Prim算法同样是用于寻找加权无向图的最小生成树(Minimum Spanning Tree, MST)的一种贪心算法。与Kruskal算法不同的是,Prim算法从一个顶点开始,逐步添加最短的边来扩展树,直到包含所有的顶点。

Prim算法基本步骤:

  1. 选择任意一个顶点作为起始顶点。
  2. 在当前树的顶点的邻接边中找到权重最小的边,将这条边添加到树中,并将新的顶点也添加进来。
  3. 重复步骤2,直到树包含所有的顶点。

这是两种算法挑选边的过程和最后结果,大家可以类比对比:

		//Kruskal算法
		W Kruskal(Self& minTree)
		{
			//初始化
			minTree._vertex = _vertex;
			minTree._index = _index;
			minTree._matrix.resize(_vertex.size());

			for (auto& e : minTree._matrix)
			{
				e.resize(_vertex.size(), MAX_W);
			}

			//优先级队列
			priority_queue<Edge, vector<Edge>, greater<Edge>> pq;

			for (size_t i = 0; i < _vertex.size(); i++)
			{
				for (size_t j = 0; j < _vertex.size(); j++)
				{
					if (i < j && _matrix[i][j] != MAX_W)
					{
						pq.push(Edge(i, j, _matrix[i][j]));
					}
				}
			}

			//拿边构造最小生成树
			W totoal = W();
			int size = 0;
			UnionFindSet ufs(_vertex.size());

			while (!pq.empty())
			{
				Edge min = pq.top();
				//出边
				pq.pop();

				//判断是否在同一集合
				if (!ufs.InSet(min._srci ,min._desi))
				{
					cout << _vertex[min._srci] << "-" << _vertex[min._desi] <<
						":" << _matrix[min._srci][min._desi] << endl;

					minTree._AddEdge(min._srci, min._desi, min._w);
					totoal += min._w;

					//合并
					ufs.Union(min._srci, min._desi);
					++size;
				}
			}

			cout << endl;

			minTree.Print();

			if (size == _vertex.size() - 1)
			{
				return totoal;
			}
			else
			{
				return W();
			}

			
		}

		W Prim(Self& minTree,const V& vertex)
		{
			//初始化
			minTree._vertex = _vertex;
			minTree._index = _index;
			minTree._matrix.resize(_vertex.size());

			for (auto& e : minTree._matrix)
			{
				e.resize(_vertex.size(), MAX_W);
			}

			//区分集合
			size_t srcIndex = FindSrci(vertex);
			vector<bool> select(_vertex.size(), false);
			vector<bool> non_select(_vertex.size(), true);

			select[srcIndex] = true;
			non_select[srcIndex] = false;

			//开始入边
			priority_queue<Edge, vector<Edge>, greater<Edge>> pq;

			for (int i = 0; i < _vertex.size(); i++)
			{
				if (_matrix[srcIndex][i] != MAX_W)
				{
					pq.push(Edge(srcIndex, i, _matrix[srcIndex][i]));
				}
			}

			size_t size = 0;
			W totoal = W();

			while (!pq.empty())
			{
				Edge min = pq.top();
				pq.pop();

				if (select[min._desi])
					continue;

				cout << _vertex[min._srci] << "-" << _vertex[min._desi] <<
					":" << _matrix[min._srci][min._desi] << endl;

				minTree._AddEdge(min._srci, min._desi, min._w);

				select[min._desi] = true;
				non_select[min._desi] = false;
				++size;
				totoal += min._w;

				//新入的顶点的边也加入到优先级队列
				for (size_t i = 0; i < _vertex.size(); i++)
				{
					if (_matrix[min._desi][i] != MAX_W && non_select[i])
					{
						pq.push(Edge(min._desi, i, _matrix[min._desi][i]));
					}
				}
			}

			minTree.Print();

			if (size == _vertex.size() - 1)
			{
				return totoal;
			}
			else
			{
				return W();
			}

		}

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

JavaWeb开发之环境准备-大合集

本文博客地址 JavaWeb开发 || 环境准备 1. 前言2. JDK8安装2.1 下载地址2.2 安装配置图示2.2.1 JDK安装2.2.2 配置系统环境变量 3. Maven安装3.1 Maven下载3.2 Maven解压及系统变量配置 4. Tomcat安装4.1 Tomcat下载4.2 Tomcat解压及系统变量配置 5. Redis安装5.1 Redis下载5.…

六西格玛绿带培训如何告别“走过场”?落地生根

近年来&#xff0c;六西格玛绿带培训已经成为了众多企业提升管理水平和员工技能的重要途径。然而&#xff0c;不少企业在实施六西格玛绿带培训时&#xff0c;往往陷入形式主义的泥潭&#xff0c;导致培训效果大打折扣。那么&#xff0c;如何避免六西格玛绿带培训变成“走过场”…

Java排序算法过程详解

标题 冒泡排序选择排序插入排序(抓牌)希尔排序归并排序 ​排序算法大体可分为两种&#xff1a; 1、比较排序&#xff0c;时间复杂度O(nlogn) ~ O(n^2)&#xff0c;主要有&#xff1a;冒泡排序&#xff0c;选择排序&#xff0c;插入排序&#xff0c;归并排序&#xff0c;堆排序&…

allure如何记录操作步骤,操作步骤不写在测试用例中,同样可以体现在allure报告,如何实现

嗨&#xff0c;我是兰若&#xff0c;今天写完用例&#xff0c;在运行用例并且生成报告的时候&#xff0c;发现报告里面没有具体的操作步骤&#xff0c;这可不行&#xff0c;如果没有具体的操作步骤的话&#xff0c;用例运行失败了&#xff0c;要怎么知道问题是出现在哪一个步骤…

【分布式数据仓库Hive】Hive的安装配置及测试

目录 一、数据库MySQL安装 1. 检查操作系统是否有MySQL安装残留 2. 删除残留的MySQL安装&#xff08;使用yum&#xff09; 3. 安装MySQL依赖包、客户端和服务器 4. MySQL登录账户root设置密码 5. 启动MySQL服务 6. 登录MySQL&#xff0c;进入数据库操作提示符 7. 授权H…

中级职称如何查询真假呢?

关于中级职称如何查询真假&#xff0c;大家都会有疑问&#xff0c;办到职称的人员肯定是想查一查手里的证书&#xff0c;那么没有证书的人员也想了解一下&#xff0c;今天甘建二告诉大家几个通俗的职称查询方式&#xff1a; 1.电话查询&#xff08;以前办理职称是这种查询方式…

大模型备案关注点最详细说明【附流程+附件】

国家网信办已经公布的通过大模型备案的有117家&#xff0c;部分已面向全社会开放服务。加上业内一些渠道透漏的消息&#xff0c;目前已有超过140个大模型获得备案。相对于算法备案&#xff0c;大模型备案名额显然更难拿到&#xff0c;很多企业在申请大模型备案的时候是一头雾水…

qiankun实现子应用tab页签切换缓存页面

实现背景 项目中是使用的jeecg-boot低代码构建的前端开发环境&#xff0c;由于后期各个模块代码越来越多&#xff0c;打包慢&#xff0c;分支管理麻烦&#xff0c;领导要求使用微前端&#xff0c;每个模块拆分为子应用。 拆分子应用 由于jeecg里面自带qiankun&#xff0c;所…

1.1.2数据结构的三要素

一.数据结构的三要素 数据结构这门课着重关注的是数据元素之间的关系&#xff0c;和对这些数据元素的操作&#xff0c;而不关心具体的数据项内容 。 1.逻辑结构 &#xff08;1&#xff09;集合结构 &#xff08;2&#xff09;线性结构 数据元素之间是一对一的关系。除了第一个…

虚幻引擎 快速的色度抠图 Chroma Key 算法

快就完了 ColorTolerance_PxRange为容差&#xff0c;这里是0-255的输入&#xff0c;也就是px单位&#xff0c;直接用0-1可以更快 Key为目标颜色

[数据集][目标检测]护目镜检测数据集VOC+YOLO格式888张1类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;888 标注数量(xml文件个数)&#xff1a;888 标注数量(txt文件个数)&#xff1a;888 标注类别…

【微信小程序开发实战项目】——花店微信小程序实战项目(4)

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;开发者-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 曼亿点 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a…

10种有效提高电子设备可靠性的PCB散热技术

在现代电子领域&#xff0c;随着器件尺寸的不断缩小和性能的不断提高&#xff0c;热管理问题日益凸显&#xff0c;不容忽视。电子设备在运行过程中产生的热量&#xff0c;如果处理不当&#xff0c;散发不了&#xff0c;就会像潜移默化的威胁一样&#xff0c;悄无声息地危及设备…

Desktop docker 部署 WordPress

Desktop Docker 部署 WordPress 之前都是在Linux里面玩的,今天看到别人在windwos下安装docker,一时兴起装了一个试试,效果一般,很吃硬盘空间和内存。 首先在docker官方下载桌面版,安装下一步一直到完成。 安装完docker会自动加入到环境变量,而且docker-compose也会一并安…

SPLL单相软件锁相环相关源代码理解-SOGI及PI系数计算

最近在学习TI的TIDA-010062&#xff08;DSP型号用的是TMS320F280049C&#xff09;&#xff0c;也就是1kW、80 Plus Titanium、GaN CCM 图腾柱无桥 PFC 和半桥 LLC&#xff08;具有 LFU&#xff09;参考设计。在整个框图中看到SPLL_1ph_SOGI的模块&#xff08;实验4&#xff1a;…

软件测试面试题集(含答案)

软件测试面试题集一、Bug基本要素 缺陷ID&#xff0c;状态&#xff0c;类型&#xff0c;所属项目&#xff0c;所属模块&#xff0c;缺陷提交时间&#xff0c;缺陷提交人&#xff08;检测者&#xff09;&#xff0c;严重程度&#xff0c;优先级别&#xff0c;缺陷描述信息&#…

【TS】TypeScript 联合类型详解:解锁更灵活的类型系统

&#x1f308;个人主页: 鑫宝Code &#x1f525;热门专栏: 闲话杂谈&#xff5c; 炫酷HTML | JavaScript基础 ​&#x1f4ab;个人格言: "如无必要&#xff0c;勿增实体" 文章目录 TypeScript 联合类型详解&#xff1a;解锁更灵活的类型系统一、联合类型的定义二…

一站式采购!麒麟信安CentOS安全加固套件上架华为云云商店

近日&#xff0c;麒麟信安CentOS安全加固套件正式上架华为云云商店&#xff0c;用户可登录华为云官网搜索“CentOS安全加固”直接采购&#xff0c;一站式获取所需资源。 麒麟信安CentOS安全加固套件已上架华为云 https://marketplace.huaweicloud.com/contents/9fe76553-8d87-…

后端部署Jar包 | 启动失败系列问题(图解-BuiId,Maven)

目录 项目的构建 打包前的准备 合理配置pox.xml文件 Build 打包方式 Maven打包方式 Jar包部署 测试后端接口 项目的构建 我的项目是SpringBoot2脚手架 先准备一个相对于的数据库依赖 数据库的任意库 Yaml配置后 才能正常在IDEA中跑起来 打包前的准备 合理配置pox.xm…

推荐的一键下载1688高保真原图信息

图片在电商中扮演着至关重要的角色。高质量的商品图片能够直观展示产品特性&#xff0c;吸引消费者注意力&#xff0c;提升购买欲望。良好的视觉呈现还能增强品牌形象&#xff0c;提高转化率。此外&#xff0c;图片是跨语言的沟通方式&#xff0c;能够克服语言障碍&#xff0c;…