【数据结构】基础:图的最小生成树(附C++源代码)

news2024/11/28 18:46:35

【数据结构】基础:图的最小生成树(附C++源代码)

摘要:将会在数据结构专题中开展关于图论的内容介绍,其中包括四部分,分别为图的概念与实现、图的遍历、图的最小生成树以及图的最短路径问题。本文主要介绍Kruskal算法(克鲁斯卡尔)与Prim算法(普里姆),二者都是通过贪心策略完成对最小生成树的生成的,需要掌握二者的思想与实现。


文章目录

  • 【数据结构】基础:图的最小生成树(附C++源代码)
    • 前言
      • 1. 图的实现方式
      • 2. 并查集
    • 一、概述
    • 二、Kruskal算法(克鲁斯卡尔)
      • 2.1 算法思想
      • 2.2 具体实例
      • 2.3 代码实现
      • 2.4 测试用例
    • 三、Prim算法(普里姆)
      • 3.1 算法实现
      • 3.2 具体实例
      • 3.3 代码实现
      • 3.4 测试用例

前言

1. 图的实现方式

本文中图的实现方法为邻接矩阵法,以下是对其类的基本描述,若需查看更加具体的内容,可以参考博客图的概念与基本实现。其重点可以概括为:

  • Direction:表示是否为有向图
  • _vertexs:记录了对应检索下的顶点元素
  • _vIndexMap:记录了检索与顶点的对应关系
  • _matrix:表示邻接矩阵

具体代码如下:

template<class V, class W, bool Direction = false, W MAX_WEIGHT = INT_MAX>
    class Graph {
        typedef Graph<V, W, Direction, MAX_WEIGHT> Self;

        private:
        vector<V> _vertexs; // 顶点集合
        map<V, int> _vIndexMap; // 顶点检索
        vector<vector<W>> _matrix; // 邻接矩阵

        public:
        Graph() = default;
        Graph(const V* vertexs,size_t vertexSize) {
            _vertexs.reserve(vertexSize);
            for (size_t i = 0; i < vertexSize; i++) {
                _vertexs.push_back(vertexs[i]);
                _vIndexMap[vertexs[i]] = i;
            }

            // 格式化
            _matrix.resize(vertexSize);
            for (auto& e : _matrix) {
                e.resize(vertexSize, MAX_WEIGHT);
            }
            //for (size_t i = 0; i < _matrix.size(); i++) {
            //	_matrix[i][i] = 0;
            //}
        }

        size_t GetVertexIndex(const V& v) {
            auto ret = _vIndexMap.find(v);
            if (ret != _vIndexMap.end()) {
                return ret->second;
            }
            else {
                throw invalid_argument("不存在的顶点");
                return -1;
            }
        }

        void AddEdge(const V& src, const V& dest, const W& weight) {
            size_t srcIndex = GetVertexIndex(src);
            size_t destIndex = GetVertexIndex(dest);
            AddEdgeByIndex(srcIndex, destIndex, weight);
        }

        void AddEdgeByIndex(size_t srcIndex, size_t destIndex, const W& weight){
            _matrix[srcIndex][destIndex] = weight;
            if (Direction == false) {
                _matrix[destIndex][srcIndex] = weight;
            }
        }
    }

2. 并查集

本文会使用到并查集这一数据结构,并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。并查集的思想是用一个数组表示了整片森林(parent),树的根节点唯一标识了一个集合,我们只要找到了某个元素的的树根,就能确定它在哪个集合里

本文对于并查集而言并非重点,因此不过多介绍。本文所使用的并查集时通过数组vectormap实现,实现了以下内容:

  • vector通过中元素若为负数表示为根,数目绝对值为集合元素的个数,正数表示存在根的检索。
  • map记录了顶点元素与数组索引的关系
  • 完成函数封装有Union合并、InSet检查是否在同一集合

完整代码示例如下:

#pragma once

#include <vector>
#include <map>
#include <iostream>

using namespace std;

template<class T>
    class UnionFindSet {
        private:
        vector<int> _set;
        map<int, T> _indexMap;
        public:
        /// <summary>
        /// 创建数组
        /// 加入索引与数组内容
        /// </summary>
        /// <param name="vectorInsert"></param>
        /// <param name="size"></param>
        UnionFindSet(vector<T> vectorInsert, int size)
            :_set(size, -1)
            {
                for (size_t i = 0; i < size; i++) {
                    cout << i<< ": "  <<vectorInsert[i] << endl;
                    _indexMap.insert(pair<T,int>(vectorInsert[i], i));
                }
            }
        /// <summary>
        /// 寻根的索引,找出对应索引进行对于根的查找
        /// 当寻根后,可以将父亲节点与祖先与根连接,降低树的高度
        /// </summary>
        /// <param name="key">键值</param>
        /// <returns></returns>
        size_t FindRootIndex(T key) {
            int index = _indexMap[key];
            while (_set[index] >= 0) {
                index = _set[index];
            }
            // 降低树的高度
            int root = index;
            int curIndex = _indexMap[key];
            while (_set[curIndex] >= 0) {
                int parentIndex = _set[curIndex];
                _set[curIndex] = root;

                curIndex = parentIndex;
            }
            return root;
        }
        /// <summary>
        /// 合并两个集合 找出两个根索引,将矮的树插入到另一棵树中
        /// </summary>
        /// <param name="key1"></param>
        /// <param name="key2"></param>
        void Union(T key1,T key2) {
            int rootIndex1 = FindRootIndex(key1);
            int rootIndex2 = FindRootIndex(key2);

            if (rootIndex1 == rootIndex2) {
                return;
            }
            if (abs(_set[rootIndex1]) < abs(_set[rootIndex2]))
                swap(rootIndex1, rootIndex2);

            _set[rootIndex1] += _set[rootIndex2];
            _set[rootIndex2] = rootIndex1;
        }

        bool InSet(T key1, T key2){
            return FindRootIndex(key1) == FindRootIndex(key2);
        }

        /// <summary>
        /// 返回并查集数目
        /// </summary>
        /// <returns></returns>
        size_t SetCount() {
            size_t count = 0;
            for (size_t i = 0; i < _set.size(); ++i){
                if (_set[i] < 0)
                    count++;
            }
            return count;
        }

    };

一、概述

连通图中的每一棵生成树,都是原图的一个极大无环子图,即:

  • 从其中删去任何一条边,生成树就不在连通;
  • 反之,在其中引入任何一条新边,都会形成一条回路。

若连通图由n个顶点组成,则其生成树必含n个顶点和n-1条边,因此构造最小生成树的准则有三条:

  • 只能使用图中的边来构造最小生成树
  • 只能使用恰好n-1条边来连接图中的n个顶点
  • 选用的n-1条边不能构成回路

构造最小生成树的方法:Kruskal算法和Prim算法。这两个算法都采用了逐步求解的贪心策略。所谓贪心算法,是指在问题求解时,总是做出当前看起来最好的选择。也就是说贪心算法做出的不是整体最优的的选择,而是某种意义上的局部最优解。贪心算法不是对所有的问题都能得到整体最优解。

二、Kruskal算法(克鲁斯卡尔)

2.1 算法思想

基本思想:任给一个有n个顶点的连通网络N={V,E},首先构造一个由这n个顶点组成、不含任何边的图G={V,NULL},其中每个顶点自成一个连通分量,其次不断从E中取出权值最小的一条边(若有多条任取其一),若该边的两个顶点来自不同的连通分量,则将此边加入到G中。如此重复,直到所有顶点在同一个连通分量上为止。

核心:每次迭代时,选出一条具有最小权值,且两端点不在同一连通分量上的边,加入生成树。

2.2 具体实例

以下截取算法导论中的实例进行说明:不断选取最小的边(依次为:h->g i->c g->f…),当可能形成环时对其不进行选择(如:i ->g),最终形成最小生成树

在这里插入图片描述

2.3 代码实现

具体实现

  • 构造最小生成树的图,其中顶点容器与检索树和原来的图相同,而邻接矩阵设置为全不可达,即不含任意边
  • 设置优先级队列,将各边推入小堆中
  • 设置并查集,将访问后的节点聚集在一个集合中,若在后续对边的访问中,两个节点都已访问,则表示已形成环,否则可以添加该边到最小生成树中
  • 在小堆取栈顶元素,判断是否符合最小生成树要求,直至边被取完为止
  • 判断形成最下生成树的边的个数,来判断是否生成成功最小生成树
W Kruskal(Self& minTree) {
    size_t n = _vertexs.size();
    // 初始化最小树的数据结构
    minTree._vertexs = this->_vertexs;
    minTree._vIndexMap = this->_vIndexMap;
    minTree._matrix.resize(n);
    for (size_t i = 0; i < n; i++) {
        minTree._matrix[i].resize(n, MAX_WEIGHT);
    }

    // 优先级队列:升序排序,排序对比看权值
    priority_queue<Edge, vector<Edge>, greater<Edge>> minQueue;
    // 加入边
    for (size_t i = 0; i < n; i++) {
        for (size_t j = 0; j < n; j++) {
            if (Direction == false) {
                //无向连通图
                if (i < j && _matrix[i][j] != MAX_WEIGHT){
                    minQueue.push(Edge(i, j, _matrix[i][j]));
                }
            }
            else {
                //有向连通图
                if (_matrix[i][j] != MAX_WEIGHT) {
                    minQueue.push(Edge(i, j, _matrix[i][j]));
                }
            }
        }
    }
    // 统计边
    int count = 0;
    // 统计权值
    W totalWeight = W();
    // 并查集:将访问过的顶点作为一个集合,否则会出现环的情况
    UnionFindSet<V> ufs(_vertexs, _vertexs.size());
    // 对于优先级队列取边,查看是否构成环
    while (!minQueue.empty()) {
        Edge minEdge = minQueue.top();
        minQueue.pop();
        // 两个顶点不在通过一个集合中
        if (!ufs.InSet(_vertexs[minEdge._srcIndex], _vertexs[minEdge._destIndex])) {
            cout << _vertexs[minEdge._srcIndex] << "->" << _vertexs[minEdge._destIndex]
                << ":" << minEdge._weight << endl;
            minTree.AddEdgeByIndex(minEdge._srcIndex, minEdge._destIndex, minEdge._weight);
            // 将两个点放入已访问的集合中
            ufs.Union(_vertexs[minEdge._srcIndex], _vertexs[minEdge._destIndex]);
            count++;
            totalWeight += minEdge._weight;
        }
        else {
            cout << "构成环:";
            cout << _vertexs[minEdge._srcIndex] << "->" << _vertexs[minEdge._destIndex] << ":" << minEdge._weight << endl;
        }
    }
    // 查看是否成功构成了n-1条边
    if (count == n - 1)
        return totalWeight;
    else
        return W();
}

2.4 测试用例

该图为算法导论中提供的案例,在上文中已经画出

void TestGraphMinTree()
{
	const char str[] = "abcdefghi";
	Graph<char, int> g(str, strlen(str));
	g.AddEdge('a', 'b', 4);
	g.AddEdge('a', 'h', 8);
	g.AddEdge('b', 'c', 8);
	g.AddEdge('b', 'h', 11);
	g.AddEdge('c', 'i', 2);
	g.AddEdge('c', 'f', 4);
	g.AddEdge('c', 'd', 7);
	g.AddEdge('d', 'f', 14);
	g.AddEdge('d', 'e', 9);
	g.AddEdge('e', 'f', 10);
	g.AddEdge('f', 'g', 2);
	g.AddEdge('g', 'h', 1);
	g.AddEdge('i', 'g', 6);
	g.AddEdge('h', 'i', 7);

	Graph<char, int> kminTree;
	cout << "Kruskal:" << g.Kruskal(kminTree) << endl;
	kminTree.Print();
	cout << endl << endl;
}

image-20230214170359450

三、Prim算法(普里姆)

3.1 算法实现

基本思想:任给一个有n个顶点的连通网络N={V,E},首先构造一个由这n个顶点组成、不含任何边的图G={V,NULL},将顶点分为两组,分别为已访问以及未访问,通过从已访问的点中筛选出与未访问的点邻接的最小权值的边,将其作为最小子树的边,加入到G中,如此重复知道边的数量为n-1条,否则构成失败。

3.2 具体实例

以下截取算法导论中的实例进行说明:最初状态中只有a点被访问,其邻接的边的另一顶点未被访问,且权值最小,该边为a-b。则加入改变,访问b点并加入对应的邻边到优先级队列中,再次从优先级队列中选边,重复该过程。

在这里插入图片描述

3.3 代码实现

  • 初始化最小生成树的数据结构,并创建两个数组记录两个集合,分别记录是否访问,并完成相应的初始化设置
  • 设置优先队列,储存已访问过的点的临边
  • 从优先级队列中,不断获取最小边,判断是否成环(标准为边的两点中只有一个被访问过),若成环选择下一条边,否则选择该边,并将其端点临边加入到优先级队列中
  • 判断是否生成树生成成功

具体代码

W Prim(Self& minTree, const W& src) {
    // 获取src的index
    size_t srcIndex = GetVertexIndex(src);
    size_t n = _vertexs.size();
    // 初始化最小生成树的数据结构
    minTree._vertexs = this->_vertexs;
    minTree._vIndexMap = this->_vIndexMap;
    minTree._matrix.resize(n);
    for (size_t i = 0; i < n; i++) {
        minTree._matrix[i].resize(n, MAX_WEIGHT);
    }

    // 创建两个数组记录两个集合,分别为是否访问
    vector<bool> vistedV(n, false);
    vector<bool> unvistedV(n, true);

    // 对于起点,已经访问
    vistedV[srcIndex] = true;
    unvistedV[srcIndex] = false;

    // 优先队列:已访问过的点的临边
    priority_queue<Edge, vector<Edge>, greater<Edge>> minQueue;
    for (size_t i = 0; i < n; ++i){
        if (_matrix[srcIndex][i] != MAX_WEIGHT){
            minQueue.push(Edge(srcIndex, i, _matrix[srcIndex][i]));
        }
    }

    size_t count = 0;
    W totalWeight = W();

    while (!minQueue.empty()) {
        // 最小边
        Edge minEdge = minQueue.top();
        minQueue.pop();

        // 看是否都是已访问过的点,是的话就形成了环
        if (vistedV[minEdge._destIndex] == true) {
            cout << "构成环:";
            cout << _vertexs[minEdge._srcIndex] << "->" 
                << _vertexs[minEdge._destIndex] << ":" << minEdge._weight << endl;
        }
        else {
            minTree.AddEdgeByIndex(minEdge._srcIndex, minEdge._destIndex, minEdge._weight);
            count++;
            vistedV[minEdge._destIndex] = true;
            unvistedV[minEdge._destIndex] = false;
            totalWeight += minEdge._weight;
            if (count == n - 1)
                break;
            // 访问后向优先队列添加临边
            for (size_t i = 0; i < n; ++i){
                if (_matrix[minEdge._destIndex][i] != MAX_WEIGHT && unvistedV[i] == true)
                {
                    minQueue.push(Edge(minEdge._destIndex, i, _matrix[minEdge._destIndex][i]));
                }
            }
        }
    }

    if (count == n - 1)
        return totalWeight;
    else
        return W();
}

3.4 测试用例

该图为算法导论中提供的案例,在上文中已经画出

void TestGraphMinTree(){
	const char str[] = "abcdefghi";
	Graph<char, int> g(str, strlen(str));
	g.AddEdge('a', 'b', 4);
	g.AddEdge('a', 'h', 8);
	g.AddEdge('b', 'c', 8);
	g.AddEdge('b', 'h', 11);
	g.AddEdge('c', 'i', 2);
	g.AddEdge('c', 'f', 4);
	g.AddEdge('c', 'd', 7);
	g.AddEdge('d', 'f', 14);
	g.AddEdge('d', 'e', 9);
	g.AddEdge('e', 'f', 10);
	g.AddEdge('f', 'g', 2);
	g.AddEdge('g', 'h', 1);
	g.AddEdge('i', 'g', 6);
	g.AddEdge('h', 'i', 7);

	Graph<char, int> pminTree;
	cout << "Prim:" << g.Prim(pminTree, 'a') << endl;
	pminTree.Print();
	cout << endl;
}

image-20230214182411107


补充:

  1. 代码将会放到:C++/C/数据结构代码链接 ,欢迎查看!
  2. 欢迎各位点赞、评论、收藏与关注,大家的支持是我更新的动力,我会继续不断地分享更多的知识!

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

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

相关文章

机器学习经典算法总结

一&#xff0c;KNN 算法 K 近邻算法&#xff08;KNN&#xff09;是一种基本分类和回归方法。KNN 算法的核心思想是如果一个样本在特征空间中的 k 个最相邻的样本中的大多数属于一个类别&#xff0c;那该样本也属于这个类别&#xff0c;并具有这个类别上样本的特性。该方法在确…

知识图谱表示

知识图谱表示 用某种语言对知识图谱进行建模&#xff0c;从而方便知识运算。 符号知识表示 &#xff1a;以符号逻辑为基础的知识表示方法。 特点&#xff1a;易于刻画显示、离散的知识。具有内生的可解释性。 不足&#xff1a;部分隐性知识不易符号化表示&#xff0c;故知识表…

Pr LOGO片头动画

哈喽&#xff0c;各位小伙伴&#xff01;今天我们来学习一下如何制作简约文字LOGO片头动画&#xff1f; 新建序列 新建一个1920*1080的序列&#xff0c;在字幕上打上我们的文字&#xff08;例&#xff1a;Ultraman&#xff09; 调出基本面板&#xff0c;调整文字的位置&…

全栈项目【尚医通】预约挂号系统项目介绍

&#x1f43c;个人主页&#xff1a;爪哇斗罗 &#x1f43c;博主介绍&#xff1a;一名打工人 &#x1f43c;签名&#xff1a;圣人之道&#xff0c;为而不争。 &#x1f43c;一起交流&#xff0c;一起进步&#xff0c;一起互动。 ​ 目录 一.项目介绍 二.技术架构 2.1 架构选…

SpringBoot集成Swagger3(powernode document)(内含源代码)

SpringBoot集成Swagger3&#xff08;powernode document&#xff09;&#xff08;内含源代码&#xff09; 源代码下载链接地址&#xff1a;https://download.csdn.net/download/weixin_46411355/87449720 目录SpringBoot集成Swagger3&#xff08;powernode document&#xff0…

数据库锁的12连问,你顶得了嘛?

前言 金三银四很快就要来啦&#xff0c;准备了数据库锁的12连问&#xff0c;相信大家看完肯定会有帮助的。 1. 为什么需要加锁 在日常生活中&#xff0c;如果你心情不好想静静&#xff0c;不想被比别人打扰&#xff0c;你就可以把自己关进房间里&#xff0c;并且反锁。这就是…

Simulink自动代码生成:如何标准化的建模?以MAB,MISRA C 2012建模规范为例

目录 为什么要规范建模 MAB&#xff0c;MISRA C2012建模规范步骤 常用的规范总结 生成代码配置 总结 为什么要规范建模 MathWorks 咨询委员会 (MAB) 规范规定了在 Simulink 和 Stateflow 中建模的重要基本规则。这些建模规范的总体目的是让控制系统模型的建模者和使用方能…

自定义ESLint规则和修复功能

这是接上一篇自定义ESLint规则开发与使用的后续扩展&#xff0c;之前文章中详细讲述了怎么创建一个自定义的规则&#xff0c;这篇文章讲述怎么实现ESLint在检测出有问题的代码时&#xff0c;怎么自动fix问题。 比如我们要检测项目中所有http的协议&#xff0c;将其替换为https协…

Power BI 存储模式介绍(导入、DirectQuery、双)

本系列的文章&#xff1a; 《Power BI windows下载安装流程&#xff09;》《Power BI 11个必学官方示例数据案例&#xff08;附下载链接&#xff09;》《Power BI 数据导入&#xff08;SQL Server、MySQL、网页数据&#xff09;》 一、背景原因 一般情况下&#xff0c;我们是…

测试开发面试基础题

1.对测试开发的理解 测试开发首先离不开测试&#xff0c;而软件测试是指&#xff0c;在规定的条件下对程序进行操作&#xff0c;以发现程序错误&#xff0c;衡量软件质量&#xff0c;并对其是否能满足设计要求进行评估的过程。 而且&#xff0c;现在不仅仅是通过手工测试来发…

安装less-loader5出现webpack版本不兼容

今天遇到一个问题&#xff1a; 安装less-loader5之后其它包提示peerDependencies WARNING&#xff0c;意思是包版本不兼容。 【难题】 虽然NPM已经很自动化了&#xff0c;但依赖问题真的是一个难题&#xff0c;无法自动解决&#xff0c;需要人工干预调整。 【解决办法】 去查…

用户选择好用的投票小程序最有用的投票小程序微信推送里投票制作教程

“夏日非遗传承活动”网络评选投票_用户选择好用的投票小程序_最有用的投票小程序用户在使用微信投票的时候&#xff0c;需要功能齐全&#xff0c;又快捷方便的投票小程序。而“活动星投票”这款软件使用非常的方便&#xff0c;用户可以随时使用手机微信小程序获得线上投票服务…

医学生考研考博太卷,一篇文章轻松助力上岸——生物信息学及R语言基础知识之向量的生成(一)

考研考博太卷了,卷不过,想没想过本科发一篇文章呢? 330分考研人淘汰390分考研人这个故事,大家应该都知道吧。 本专栏带你六个月内,搞定一篇文章,本科生发文章也很容易。 在卷考研的同时,再卷一篇SCI,你就是新一任卷王。 本专栏教你不用花钱发一篇生信文章,从三个方…

带你了解软件测试是做什么的

软件测试是互联网技术中一门重要的学科&#xff0c;它是软件生命周期中不可或缺的一个环节&#xff0c;担负着把控、监督软件的质量的重任。 人才稀缺&#xff0c;对于求职者来说就意味着机会。但是很多想学习软件测试的人对这个学科并不了解&#xff0c;也不知道该如何学习&a…

Vue3的新特性

文章目录1 生命周期的变化2 使用proxy代替defineProperty2.1 Object.defineProperty()语法2.2 Proxy的语法3 Diff算法的提升3.1 以往的渲染策略3.2 Vue3的突破4 TypeScript的支持5 优化打包体积6 新的响应性 API6.1 reactive()6.2 <script setup>6.3 nextTick()6.4 react…

【半监督医学图像分割 2022 MICCAI】CLLE 论文翻译

文章目录【半监督医学图像分割 2022 MICCAI】CLLE 论文翻译摘要1. 简介2. 方法2.1 半监督框架概述2.2 监督局部对比学习2.3 下采样和块划分3. 实验4. 结论【半监督医学图像分割 2022 MICCAI】CLLE 论文翻译 论文题目&#xff1a;Semi-supervised Contrastive Learning for Labe…

Kafka 消息队列

目录主流的消息队列消息队列的应用场景缓存/肖锋解耦异步处理KafkaKafka的定义Kafka的底层基础架构Kafka分区如何保证Leader选举Kafka分区如何保证Leader和Follower数据的一致性Kafka 中消费者的消费方式Kafka 高效读写数据的原因&#xff08;高性能吞吐的原因&#xff09;&…

chatGPT爆火让我们反思——人工智能是新的加密货币吗?

核冬天技术末日到来了&#xff0c;只有人工智能幸免于难。峰值 AI 指标无处不在。它能保持加密失去的信念吗&#xff1f;作者&#xff1a;John Luttig 翻译: Chainwise核冬天技术末日到来了&#xff1a;软件、SPAC、金融科技和加密货币都进入了深度冻结状态。AI 可能是唯一穿着…

JavaWeb_RequestResponse

目录 一、概述 二、Request对象 1.Request继承体系 2.Request获取请求数据 ①获取请求行数据 ②获取请求头数据 ③获取请求体数据 ④获取请求参数 3.Request请求转发 三、Response 1.Response设置响应数据功能 ①响应行 ②响应头 ③响应体 2.请求重定向 3.路径问…

原生开发 之 微信小程序

目录 一、前期预备 1. 预备知识 ​2. 注册账号 - 申请AppID 3. 下载小程序开发工具 4. 小程序项目结构 ​5. 小程序的MVVM架构 二、创建小程序项目 1. 查看注册的appId ​2. 创建项目 ​3. 新建页面 01 - 创建text页面文件夹 ​02 - 新建text的page ​03 - 在app.json中配置 ​…