【C++ 实现】图论概念,最小生成树,单/多源最短路径实现


数据结构表示图

首先节点的存取,V是节点key,vector<pair<V,V>> map;其实已经能表达一个图了,但是这样保存节点对我们使用来说会导致复杂度高。

常用保存节点的方式,有矩阵和邻接表。
矩阵的优点:O(1) 时间找到两点是否相连以及他们的权值。
矩阵的缺点:找一点相邻的所有节点的时候是O(N)的,即会遍历到不相连的两两节点。

图还分有向图和无向图,一般来说有向图存出边即可。

template<class V, class W, bool Direction = false> V表示节点的类型,W表示权值的类型,Direction表示是否是无向图。
图一般提供的函数,构造函数,将图的每一个节点的距离初始化;
AddEdge函数:将两个节点建立联系,附上权值;
GetVertexIndex: 将对应的图的节点转化为下标

template <class V, class W, W MAX_W = INT_MAX, bool Direction = false>
class Graph
{
public:
    typedef Graph<V, W, MAX_W, Direction> Self;
    Graph() = default;
    Graph(const V *vertexs, size_t n)
    {
        _vertexs.reserve(n);
        for (size_t i = 0; i < n; ++i)
        {
            _vertexs.push_back(vertexs[i]);
            _vIndexMap[vertexs[i]] = i;
        }
        // MAX_W 作为不存在边的标识值
        _matrix.resize(n);
        for (auto &e : _matrix)
        {
            e.resize(n, MAX_W);
        }
    }
    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(size_t srci, size_t dsti, const W &w)
    {
        _matrix[srci][dsti] = w;
        if (Direction == false)
        {
            _matrix[dsti][srci] = w;
        }
    }
    void AddEdge(const V &src, const V &dst, const W &w)
    {
        size_t srci = GetVertexIndex(src);
        size_t dsti = GetVertexIndex(dst);
        _AddEdge(srci, dsti, w);
    }
           
        void Print()
    {
        // 打印顶点和下标映射关系
        for (size_t i = 0; i < _vertexs.size(); ++i)
        {
            cout << _vertexs[i] << "-" << i << " ";
        }
        cout << endl
             << endl;
        cout << " ";
        for (size_t i = 0; i < _vertexs.size(); ++i)
        {
            cout << i << " ";
        }
        cout << endl;
        // 打印矩阵
        for (size_t i = 0; i < _matrix.size(); ++i)
        {
            cout << i << " ";
            for (size_t j = 0; j < _matrix[i].size(); ++j)
            {
                if (_matrix[i][j] != MAX_W)
                    cout << _matrix[i][j] << " ";
                else
                    cout << "#"
                         << " ";
            }
            cout << endl;
        }
        cout << endl
             << endl;
        // 打印所有的边
        for (size_t i = 0; i < _matrix.size(); ++i)
        {
            for (size_t j = 0; j < _matrix[i].size(); ++j)
            {
                if (i < j && _matrix[i][j] != MAX_W)
                {
                    cout << _vertexs[i] << "-" << _vertexs[j] << ":" << _matrix[i][j] << endl;
                }
            }
        }
    }
            private : map<V, size_t> _vIndexMap;
    vector<V> _vertexs; // 顶点集合
    vector<vector<W>> _matrix;
      // 存储边集合的矩阵
};
void TestGraph()
{
    Graph<char, int, INT_MAX, true> g("0123", 4);
    g.AddEdge('0', '1', 1);
    g.AddEdge('0', '3', 4);
    g.AddEdge('1', '3', 2);
    g.AddEdge('1', '2', 9);
    g.AddEdge('2', '3', 8);
    g.AddEdge('2', '1', 5);
    g.AddEdge('2', '0', 3);
    g.AddEdge('3', '2', 6);
    g.Print();
}
}
namespace LinkTable
{
    template <class W>
    struct LinkEdge
    {
        int _srcIndex;
        int _dstIndex;
        W _w;
        LinkEdge<W> *_next;
        LinkEdge(const W &w)
            : _srcIndex(-1), _dstIndex(-1), _w(w), _next(nullptr)
        {
        }
    };
    template <class V, class W, bool Direction = false>
    class Graph
    {
        typedef LinkEdge<W> Edge;

    public:
        Graph(const V *vertexs, size_t n)
        {
            _vertexs.reserve(n);
            for (size_t i = 0; i < n; ++i)
            {
                _vertexs.push_back(vertexs[i]);
                _vIndexMap[vertexs[i]] = i;
            }
            _linkTable.resize(n, nullptr);
        }
        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 &dst, const W &w)
        {
            size_t srcindex = GetVertexIndex(src);
            size_t dstindex = GetVertexIndex(dst);
            // 0 1
            Edge *sd_edge = new Edge(w);
            sd_edge->_srcIndex = srcindex;
            sd_edge->_dstIndex = dstindex;
            sd_edge->_next = _linkTable[srcindex];
            _linkTable[srcindex] = sd_edge;
            // 1 0
            // 无向图
            if (Direction == false)
            {
                Edge *ds_edge = new Edge(w);
                ds_edge->_srcIndex = dstindex;
                ds_edge->_dstIndex = srcindex;
                ds_edge->_next = _linkTable[dstindex];
                _linkTable[dstindex] = ds_edge;
            }
        }

    private:
        map<string, int> _vIndexMap;
        vector<V> _vertexs;        // 顶点集合
        vector<Edge *> _linkTable; // 边的集合的临接表
    };
    void TestGraph()
    {
        string a[] = {"张三", "李四", "王五", "赵六"};
        Graph<string, int> g1(a, 4);
        g1.AddEdge("张三", "李四", 100);
        g1.AddEdge("张三", "王五", 200);
        g1.AddEdge("王五", "赵六", 30);
    }
}

最小生成树


什么是最小生成树
在给定一张无向图,如果在它的子图中,任意两个顶点都是互相连通,并且是一个树结构,那么这棵树叫做生成树。当连接顶点之间的图有权重时,权重之和最小的树结构为最小生成树!

Kruskal

  • 选最短的边,且不能构成回路,会用到并查集。因为需要找两个点是否已经存在路径当中。
  • AddEdge可以重载,但是由于V可能就是int,就会造成错误,所以采用换个函数名的方式达到复用的效果。
  • 总之细节很多,实现完看看代码。有不是连通图的情况也需要判断。
//传入参数为一个图,是一个输入型参数,最终图的数值会保存在该图当中。
//返回一个最终的权值总和
int Kruskal(Self& minTree)
{
	//首先初始化最小生成树
	minTree._vertex = _vertex;
	minTree._vertex2index = _vertex2index;
	size_t len = _weightmatrix.size();
	minTree._weightmatrix.resize(len, vector<W>(len, MaxSize));
	//Kruskal算法关注的是边的关系,需要每次取出权重最小的边
	priority_queue<Edge,vector<Edge>,greater<Edge>> pq;
	//初始化优先级队列
	for (int i = 0; i < len; ++i)
	{
		for (int j = 0; j < len; ++j)
		{
			//这个点存在有效权值则入pq,注意这里只用入矩阵的一半即可
			if (i < j && _weightmatrix[i][j] != MaxSize)
			{
				pq.push(Edge(i, j, _weightmatrix[i][j]));
			}
		}
	}
	//此时就有了一个优先级队列保存最小的节点。
	//需要拿出优先级队列的节点并且需要是不能构成环的
	//全局贪心,每次找最小的边进行合并,需要注意不能在一个集合当中,所以可以使用并查集来,且需要保存的是点
	//将所有找到的边合并起来就是答案
	UnionFindSet unionset(_vertex.size());
	W weight = W();
	int i = 1;//边是比点少一条的。
	while (!pq.empty())
	{
		Edge edge = pq.top();
		pq.pop();
		if (unionset.FindRoot(edge._srci) == unionset.FindRoot(edge._desti))
		{
			cout << "构成环: " << _vertex[edge._srci] << "->" << _vertex[edge._desti] << endl;
			continue;
		}
		i++;
		cout << "Add:" << edge._srci << "-> " << edge._desti << endl;
		minTree.Add2Weight(_vertex[edge._srci], _vertex[edge._desti], edge._weight);
		unionset.Union(edge._srci, edge._desti);
		cout << _vertex[edge._srci] << "->" << _vertex[edge._desti] << endl;
		weight += edge._weight;
	}
	if (i != _vertex.size())
	{
		//这个时候表示不是一个连通图
		cout << "不是连通图" << endl;
	}
	return weight;
}

Prim


在这里插入图片描述
步骤:

  • 先加入const V& v四周围的边入优先级队列(小堆),每次从头部获取一个最短的边,记录为访问,添加新增节点的四周围的边。
  • 为了防止添加的边有重复,我们采用vector<bool>来记录每一个顶点,记录每一个添加进来的顶点。

当然选的边不同,结果也有可能相同。
在这里插入图片描述

//Prim算法是基于局部的贪心
int Prim(Self& minTree, const V& v)
{
	//初始化最小生成树
	minTree._vertex = _vertex;
	minTree._vertex2index = _vertex2index;
	int len = _vertex.size();
	minTree._weightmatrix.resize(len, vector<W>(len, MaxSize));

	//初始化优先级队列,只不过入一个节点周围的
	priority_queue<Edge,vector<Edge>,greater<Edge>> pq;
	int srci = VertexToIndex(v);
	for (int i = 0; i < len; ++i)
	{
		if (_weightmatrix[srci][i] != MaxSize)
			pq.push(Edge(srci, i, _weightmatrix[srci][i]));
	}
	//初始化访问的列表,这个可以用作区分集合,出边不在visited就不会构成环
	vector<bool> visited(len, false);
	visited[srci] = true;
	W weight = W();//默认权值

	int size = 0;//记录边数
	//从优先级队列取出一个点作为最小的值,这个位置被认为是总体权值最小所加的一条边。
	while (!pq.empty())
	{
		Edge indexEdge = pq.top();
		pq.pop();
		//如果该边终点已经访问过,那么就一定会构成环。
		if (visited[indexEdge._desti])
		{
			cout << "形成回路: " << _vertex[indexEdge._srci] << "->" << _vertex[indexEdge._desti] << endl;
			//该点若已经访问过,说明是入边,此时加入该点会造成环
			continue;
		}
		cout << "加入集合: " << _vertex[indexEdge._srci] << "->" << _vertex[indexEdge._desti] << endl;
		weight += _weightmatrix[indexEdge._srci][indexEdge._desti];
		//入四周围的边
		for (int i = 0; i < _weightmatrix[indexEdge._desti].size(); ++i)
		{
			//该点没有访问过并且是可以到达的则入集合
			if (_weightmatrix[indexEdge._desti][i] != MaxSize && visited[i] == false)
			{
				pq.push(Edge(indexEdge._desti, i, _weightmatrix[indexEdge._desti][i]));
				cout << "加入" << _vertex[indexEdge._desti] << " 后新增了" << _vertex[i] << endl;
			}
		}
		minTree.Add2Weight(_vertex[indexEdge._srci], _vertex[indexEdge._desti], indexEdge._weight);
		visited[indexEdge._desti] = true;
		size++;
	}
	//若size最终为边数说明边都入进来了。
	if (size == _vertex.size()-1)
	{
		//表明了到达这里的时候没有找到一个点
		cout << "是连通图" << endl;
		return weight;
	}
	else
	{
		cout << "不是联通图" << endl;
		return W();
	}

	return weight;
}

最短路径


从A点到其他点的最短距离。
Dijkstra无法解决带负权值的,后续的算法无法解决带负权回路。

Dijkstra

找的起始边,用邻接表和邻接矩阵的有各自的优缺点。

每次用最短的路径更新周围的节点进行松弛操作。
每一次选取一个点,作为到达该点的最短点,由该点更新周围其他点。
缺点:有负权边用Dijkstra会出错。
在这里插入图片描述

//Dist数组就是告知从src到对应下标所要的代价(权值),path就是该节点父亲节点的下标
void Dijkstra(const V& src, vector<int>& dist, vector<int>& path)
{
	int index = VertexToIndex(src);
	size_t n = _vertex.size();
	dist.resize(n, MaxSize);
	path.resize(n, -1);
	path[index] = index;//自己就是自己的父节点
	dist[index] = 0;
	int len = path.size();
	vector<bool> visited(len, false);
	int size = len;
	while (size--)
	{
		//用index去跟新其他节点的路径
		for (int i = 0; i < n; ++i)
		{
			if (_weightmatrix[index][i] != MaxSize && dist[i] > dist[index] + _weightmatrix[index][i])
			{
				//更新其他路径需要保证更新完更小
				dist[i] = dist[index] + _weightmatrix[index][i];
				//更新父亲
				path[i] = index;
			}
		}
		//更新index
		visited[index] = true;
		int minNum = INT_MAX;

		for (int i = 0; i < n; ++i)
		{
			if (!visited[i] && dist[i] < minNum)
			{
				index = i;
				minNum = dist[i];
			}
		}
		if (minNum == INT_MAX)
		{
			break;
		}
	}
	if (size != 0)
	{
		cout << "不是连通图" << endl;
	}
}

Bellman-Ford算法

Dijkstra算法只能用来解决正权图的单源最短路径问题,但有些题目会出现负权图。这时这个算法
就不能帮助我们解决问题了,而bellman—ford算法可以解决负权图的单源最短路径问题。它的
优点是可以解决有负权边的单源最短路径问题,而且可以用来判断是否有负权回路。它也有明显
的缺点,它的时间复杂度 O(N*E) (N是点数,E是边数)普遍是要高于Dijkstra算法O(N²)的。像这里
如果我们使用邻接矩阵实现,那么遍历所有边的数量的时间复杂度就是O(N^3),这里也可以看出
来Bellman-Ford就是一种暴力求解更新。

  • 优化:第一个轮次进行更新的边才会影响其他路径,只需要拿这些边去更新其他边(SPFA优化,队列优化)。但是对于时间复杂度并没有得出一个结论。最坏情况O(N^3),最好情况O(N^2)
  • 循环提前跳出优化
    可以用SPFA优化,采用队列优化。
    在这里插入图片描述
    用 (i,j)存在权值的情况去更新其他边。由于每次遍历整个邻接矩阵就好了。
    但是需要遍历n回,n是点的个数。
    注意每一个点dist[到自己] = 0,一开始可以设置为无穷大,根据给的顶点进行更新就可以了。dist[index] = W();//自己到自己的权值为0
    a->b假设需要更新,最多用len条边更新。
// 时间复杂度:O(N^3) 空间复杂度:O(N)
bool BellmanFord(const V& src, vector<W>& dist, vector<int>& pPath)
{
	//从src更新其他节点,首先看src能够更新哪些节点
	int len = _vertex.size();
	dist.resize(len, MaxSize);
	pPath.resize(len,-1);

	int index = VertexToIndex(src);
	dist[index] = W();//自己到自己的权值为0


	//如果只更新一次,可能出现后续的节点影响了前面的节点的情况,一个节点最多经过k-2个节点,需要更新k次
	for (int k = 0; k < len; ++k)
	{
		bool flag = true;//如果不需要跟新就可以跳出来
		cout << "更新第:" << k << "轮" << endl;
		for (int i = 0; i < len; ++i)
		{
			//更新已经能够到达的路径周围的节点
			if (dist[i] == MaxSize)
				continue;

			//这里的i就是需要更新周围节点
			for (int j = 0; j < len; ++j)
			{
				if (i != j && _weightmatrix[i][j] != MaxSize)
				{
					//表示这个点是有联系的
					if (dist[j] > dist[i] + _weightmatrix[i][j])
					{
						cout << _vertex[i] << "->" << _vertex[j] << ":" << _weightmatrix[i][j] << endl;
						flag = false;
						//就可以更新
						dist[j] = dist[i] + _weightmatrix[i][j];
						//更新父路径实际上是从i这个节点的父路径来的,观察这里
						pPath[j] = i;
					}
				}
			}
		}
		if (flag)
			break;
	}
	//检测是否有负权回路,检测方法为再更新一次即可。
	bool flag = false;
	for (int i = 0; i < len; ++i)
	{
		//更新已经能够到达的路径周围的节点
		if (dist[i] == MaxSize)
			continue;

		//这里的i就是需要更新周围节点
		for (int j = 0; j < len; ++j)
		{
			if (i != j && _weightmatrix[i][j] != MaxSize)
			{
				//表示这个点是有联系的
				if (dist[j] > dist[i] + _weightmatrix[i][j])
				{
					flag = true;
					break;
				}
			}
		}
		if (flag)
			return false;//有负权回路

		return true;
	}

}

多源最短路径:FloydWarshall


dist数组和pPath数组得是二维的,就能记录任意两个点。结构和前面的有所不同。 距离矩阵能存储任意两个点的距离。
dist数组在前面都是以一个特定的点出发,是我们传参决定的。
FloydWarshall 是取中间点进行更新。本质是用了动态规划。
不需要像Dijstra,Bellman-Ford一个在距离矩阵取,一个在邻接矩阵取。而是都在距离矩阵dist取即可。
在这里插入图片描述
三维当中的k是经过多少个点的意思。情况就是上述的两种。他虽然是三维空间来表示,但是实现的时候由于中间节点k也可以查二维矩阵就可以了,所以就用二维的矩阵表示。

若是不带负权回路,用Dijstra遍历每一个顶点也是O(N^3),但是FloydWarshall是可以解决负权回路的。
而Bellman-Ford算法的话效率太低了。所以多源最短路径横空出世。

Floyd-Warshall算法是解决任意两点间的最短路径的一种算法。
Floyd算法考虑的是一条最短路径的中间节点,即简单路径p={v1,v2,…,vn}上除v1和vn的任意节
点。
设k是p的一个中间节点,那么从i到j的最短路径p就被分成i到k和k到j的两段最短路径p1,p2。p1
是从i到k且中间节点属于{1,2,…,k-1}取得的一条最短路径。p2是从k到j且中间节点属于{1, 2,…,k-1}取得的一条最短路径。
在这里插入图片描述

在这里插入图片描述
左边是距离矩阵,右边是父路径矩阵。

  • 初始化
  • 直接相连的边更新,自己到自己初始化成0
  • 注意父路径vvpPath[i][j] 若是经过了k,则是k,所以需要改变,并且即使是vvDist[i][j] + vvDist[k][j]途中经过了其他的节点,因为我们要找的与j相连的上一个邻接点。k->j不一定是直接相连,若是则是k,否则就是其他。
  • 由于起始点和终点都有可能变,所以中间节点都是需要遍历的,也就是for(int k = 0;k < len;++k)如同i->j最多经过n-2个点。
void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath)
		{
			vvDist = _weightmatrix;
			int len = vvDist.size();
			//vvDist是权值矩阵
			//vvpPath用来表示到达i,j位置的父亲
			vvpPath.resize(len, vector<int>(len, -1));
			// 直接相连的边更新一下
			for (size_t i = 0; i < len; ++i)
			{
				for (size_t j = 0; j < len; ++j)
				{
					if (_weightmatrix[i][j] != MaxSize)
					{
						vvpPath[i][j] = i;
					}
					if (i == j)
					{
						vvDist[i][j] = W();
					}
				}
			}
			for (int k = 0; k < len; ++k)
			{
				for (int i = 0; i < len; ++i)
				{
					//每次更新从i出去的点
					for (int j = 0; j < len; ++j)
					{
					
							// k 作为的中间点尝试去更新i->j的路径
						if (vvDist[i][k] != MaxSize && vvDist[k][j] != MaxSize
							&& vvDist[i][k] + vvDist[k][j] < vvDist[i][j])
						{
							vvDist[i][j] = vvDist[i][k] + vvDist[k][j];
							vvpPath[i][j] = vvpPath[k][j];
						}
					}
				}
			}
			// 打印权值和路径矩阵观察数据
			for (size_t i = 0; i < len; ++i)
			{
				for (size_t j = 0; j < len; ++j)
				{
					if (vvDist[i][j] == MaxSize)
					{
						//cout << "*" << " ";
						printf("%3c", '*');
					}
					else
					{
						//cout << vvDist[i][j] << " ";
						printf("%3d", vvDist[i][j]);
					}
				}
				cout << endl;
			}
			cout << endl;

			for (size_t i = 0; i < len; ++i)
			{
				for (size_t j = 0; j < len; ++j)
				{
					//cout << vvParentPath[i][j] << " ";
					printf("%3d", vvpPath[i][j]);
				}
				cout << endl;
			}
			cout << "=================================" << endl;		
		}

总结


图论到此为止~

  • 喜欢就收藏
  • 认同就点赞
  • 支持就关注
  • 疑问就评论
  • 6
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

^jhao^

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

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

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

打赏作者

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

抵扣说明:

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

余额充值