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

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

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


前言

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. 欢迎各位点赞、评论、收藏与关注,大家的支持是我更新的动力,我会继续不断地分享更多的知识!
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fat one

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值