【数据结构高阶】图

目录

一、图的基本概念

二、 图的存储结构

2.1 邻接矩阵

2.2.1 邻接矩阵存储模式的代码实现

2.2.2 邻接矩阵存储的优缺点

2.2 邻接表

2.2.1 无向图的邻接表 

2.2.2 有向图的邻接表 

 2.2.3 邻接表存储模式的代码实现

2.2.4 邻接表存储的优缺点

三、图的遍历

3.1 图的广度优先遍历(BFS)

3.2 图的深度优先遍历(DFS)

3.2 非连通图的遍历 

四、最小生成树

4.1 Kruskal算法

4.2 Prim算法

五、最短路径

5.1 Dijkstra算法

5.2 Bellman-Ford算法

5.3 Floyd-Warshall算法

六、图的完整代码

6.1 实现代码

6.2 测试代码


一、图的基本概念

是由顶点集合及顶点间的关系组成的一种数据结构:G = (V, E),其中:

顶点集合V = {x|x属于某个数据对象集}是有穷非空集合; E = {(x,y)|x,y属于V}或者E = {|x,y属于V && Path(x, y)}是顶点间关系的有穷集合,也叫做边的集合。

(x, y)表示x到y的一条双向通路,即(x, y)是无方向的;Path(x, y)表示从x到y的一条单向通路,即Path(x, y)是有方向的。

由上面图的定义我们可以知道我们之前所学的树结构是一中无环的特殊图

顶点和边:图中结点称为顶点,第i个顶点记作vi。两个顶点vi和vj相关联称作顶点vi和顶点vj之间有一条边,图中的第k条边记作ek,ek = (vi,vj)或<vi,vj>。

有向图和无向图:在有向图中,顶点对是有序的,顶点对称为顶点x到顶点y的一条边(弧),和是两条不同的边,比如下图G3和G4为有向图。在无向图中,顶点对(x, y) 是无序的,顶点对(x,y)称为顶点x和顶点y相关联的一条边,这条边没有特定方向,(x, y)和(y,x) 是同一条边,比如下图G1和G2为无向图。注意:无向边(x, y)等于有向边和。

无向图通常用来表示强相关关系,例如qq、微信的联系人

有向图通常用来表示弱相关关系,例如抖音、微博的博主/粉丝之间的联系

完全图:在有n个顶点的无向图中,若有n * (n-1)/2条边(等差数列),即任意两个顶点之间有且仅有一条边, 则称此图为无向完全图,比如上图G1;在n个顶点的有向图中,若有n * (n-1)条边,即任意两个顶点之间有且仅有方向相反的边,则称此图为有向完全图,比如上图G4。

邻接顶点:在无向图中G中,若(u, v)是E(G)中的一条边,则称u和v互为邻接顶点,并称边(u,v)依 附于顶点u和v;在有向图G中,若是E(G)中的一条边,则称顶点u邻接到v,顶点v邻接自顶点u,并称边与顶点u和顶点v相关联。

顶点的度:顶点v的度是指与它相关联的边的条数,记作deg(v)。在有向图中,顶点的度等于该顶 点的入度与出度之和,其中顶点v的入度是以v为终点的有向边的条数,记作indev(v);顶点v的出度 是以v为起始点的有向边的条数,记作outdev(v)。因此:dev(v) = indev(v) + outdev(v)。注意:对于无向图,顶点的度等于该顶点的入度和出度,即dev(v) = indev(v) = outdev(v)。

路径:在图G = (V, E)中,若从顶点vi出发有一组边使其可到达顶点vj,则称顶点vi到顶点vj的顶点序列为从顶点vi到顶点vj的路径。

路径长度:对于不带权的图,一条路径的路径长度是指该路径上的边的条数;对于带权的图,一 条路径的路径长度是指该路径上各个边权值的总和。

简单路径与回路:若路径上各顶点v1,v2,v3,…,vm均不重复,则称这样的路径为简单路径。若路径上第一个顶点v1和最后一个顶点vm重合,则称这样的路径为回路或环。

子图:设图G = {V, E}和图G1 = {V1,E1},若V1属于V且E1属于E,则称G1是G的子图。 

连通图:在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任意一 对顶点都是连通的,则称此图为连通图。

强连通图:在有向图中,若在每一对顶点vi和vj之间都存在一条从vi到vj的路径,也存在一条从vj 到 vi的路径,则称此图是强连通图。

生成树:一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的生成树有n个顶点和n-1条边。

二、 图的存储结构

因为图中既有节点,又有边(节点与节点之间的关系),因此,在图的存储中,只要保存节点和边关系即可。节点保存比较简单,只需要一段连续空间即可,边的关系的存储就相对复杂一些

主要有两种方法来存储节点和边的关系:

2.1 邻接矩阵

因为节点与节点之间的关系就是连通与否,即为0(不连通)或者不为0(连通),因此邻接矩阵(二维数组)即是:先用一个数组将定点保存,然后采用矩阵来表示节点与节点之间的关系。

我们可以发现无向图的邻接矩阵是对称的(关于矩阵的对角线对称),第i行(列)元素之和,就是顶点i的度;有向图的邻接矩阵则不一定是对称的,第i行(列)元素之后就是顶点i的出(入)度。

但是在具体使用时:如果边带有权值,并且两个节点之间是连通的,上图中的边的关系就用权值代替,如果两个顶点不通,则使用无穷大代替(在代码中没有无穷大,用特殊值来代替):

2.2.1 邻接矩阵存储模式的代码实现

代码实现: 

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

using namespace std;

namespace matrix//邻接矩阵存储模式
{
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>//V为顶点类型,W为边的权值类型,MAX_W表示两个顶点到达不了的权值(用户可根据具体情况传入),Direction传入的真假值表示是否为有向图
	class Graph
	{
	public:
		Graph(const V* a, size_t n)//传入顶点的集合和顶点数量
		{
			_vertexs.reserve(n);
			for (int i = 0; i < n; ++i)//存储顶点
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;//建立顶点对于邻接矩阵元素下标的映射关系
			}
			_matrix.resize(n);
			for (int i = 0; i < n; ++i)//初始化邻接矩阵
			{
				_matrix[i].resize(n, MAX_W);
			}
		}

		int GetVertexIndex(const V& v)//返回顶点对应的矩阵下标
		{
			auto find = _indexMap.find(v);
			if (find != _indexMap.end())
			{
				return find->second;
			}
			else
			{
				cout << "顶点不存在" << endl;
				return -1;
			}
		}

		void AddEdge(const V& src, const V& dst, const W& w)
		{
			int s = GetVertexIndex(src);
			int d = GetVertexIndex(dst);
			if (d == s || s == -1 || d == -1)
			{
				cout << "插入的边不合法" << endl;
				return;
			}
			_matrix[s][d] = w;
			if (Direction == false)//无向图将两端都链接起来
			{
				_matrix[d][s] = w;
			}
		}

		void Print()//图的打印
		{

			cout << "\t";
			for (int i = 0; i < _vertexs.size(); ++i)
			{
				cout << _vertexs[i] << "(" << i << ")" << "\t";
			}
			cout << endl;
			for (int i = 0; i < _matrix.size(); ++i)
			{
				cout << _vertexs[i] << "(" << i << ")" << "\t";
				for (int j = 0; j < _matrix[i].size(); ++j)
				{
					if (_matrix[i][j] == MAX_W)
					{
						cout << "∞\t";
					}
					else
					{
						cout << _matrix[i][j] << "\t";
					}
				}
				cout << endl;
			}
		}

	private:
		vector<V> _vertexs;//存储顶点
		map<V, int> _indexMap;//存储顶点所对应的邻接矩阵的下标的映射
		vector<vector<W>> _matrix;//邻接矩阵
	};
}

测试代码:

void TestGraph()
{
	matrix::Graph<char, int, INT_MAX, true > g("abcd", 4);
	g.AddEdge('a', 'b', 1);
	g.AddEdge('a', 'd', 4);
	g.AddEdge('b', 'd', 2);
	g.AddEdge('b', 'c', 9);
	g.AddEdge('c', 'd', 8);
	g.AddEdge('c', 'a', 5);
	g.AddEdge('c', 'a', 3);
	g.AddEdge('d', 'c', 6);
	g.Print();
}

int main()
{
	TestGraph();
	return 0;
}

测试结果:

2.2.2 邻接矩阵存储的优缺点

邻接矩阵优点:

● 适合表示稠密图:对于稠密图(边数接近于顶点数的平方),邻接矩阵占用空间小,查找任意两个顶点间是否相邻的时间复杂度为 O(1)。
● 方便进行图的运算:可以直观地进行图的运算,如判断是否存在某条边、计算顶点的度等。

邻接矩阵缺点:

● 浪费空间:对于稀疏图(边数远小于顶点数的平方),邻接矩阵会浪费大量空间存储不存在的边。
● 插入和删除边的效率低:插入和删除边时需要更新整个矩阵,时间复杂度为 O(V^2),其中 V为顶点数。

2.2 邻接表

邻接表:使用数组表示顶点的集合,使用链表表示 (类似于哈希桶)

2.2.1 无向图的邻接表 

无向图中同一条边在邻接表中出现了两次;如果想知道顶点vi的度,只需要知道顶点vi边链表集合中结点的数目即可

2.2.2 有向图的邻接表 

我们可以看到有向图的邻接表有两个:入边表(在实际使用时一般会省略)和出边表

● 有向图中每条边在邻接表中只出现一次,与顶点vi对应的邻接表所含结点的个数,就是该顶点的出度,也称出度表

● 要得到vi顶点的入度:有入边表时,只需要知道顶点vi入边表集合中结点的数目即可;无入边表时,必须检测其他所有顶点对应的边链表,看有多少边顶点的dst取值是i

 2.2.3 邻接表存储模式的代码实现

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

using namespace std;

namespace link_table//邻接表存储模式
{
	template<class W>
	struct Edge//存储边的关系
	{
		int _dsti;//目标点的下标
		W _w;//权值
		struct Edge<W>* _next;

		Edge(const int dsti,const W& w)
			:_dsti(dsti),
			_w(w),
			_next(nullptr)
		{}
	};

	template<class V, class W, bool Direction = false>//V为顶点类型,W为边的权值类型,Direction传入的真假值表示是否为有向图
	class Graph
	{
		typedef Edge<W> Edge;

	public:
		Graph(const V* a, size_t n)//传入顶点的集合和顶点数量
		{
			_vertexs.reserve(n);
			for (int i = 0; i < n; ++i)//存储顶点
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;//建立顶点对于邻接矩阵元素下标的映射关系
			}
			_tables.resize(n, nullptr);//初始化邻接表
		}

		int GetVertexIndex(const V& v)//返回顶点对应的矩阵下标
		{
			auto find = _indexMap.find(v);
			if (find != _indexMap.end())
			{
				return find->second;
			}
			else
			{
				cout << "顶点不存在" << endl;
				return -1;
			}
		}

		void AddEdge(const V& src, const V& dst, const W& w)
		{
			int s = GetVertexIndex(src);
			int d = GetVertexIndex(dst);
			if (d == s || s == -1 || d == -1)
			{
				cout << "插入的边不合法" << endl;
				return;
			}
			//头插
			Edge* eg = new Edge(d, w);
			eg->_next = _tables[s];
			_tables[s] = eg;
			if (Direction == false)//无向图将两端都链接起来
			{
				Edge* eg = new Edge(s, w);
				eg->_next = _tables[d];
				_tables[d] = eg;
			}
		}

		void Print()//图的打印
		{
			for (int i = 0; i < _tables.size(); ++i)
			{
				cout << _vertexs[i] << "(" << i << "):";
				Edge* cur = _tables[i];
				while (cur)
				{
					cout <<"[" << _vertexs[cur->_dsti] << "(" << cur->_dsti << "),w:" << cur->_w << "] ";
					cur = cur->_next;
				}
				cout << endl;
			}
		}

	private:
		vector<V> _vertexs;//存储顶点
		map<V, int> _indexMap;//存储顶点所对应的邻接表的下标的映射
		vector<Edge*> _tables;//邻接表(出边表)
	};
}

测试代码:

void TestGraph2()
{
	string a[] = { "张三", "李四", "王五", "赵六" };
	link_table::Graph<string, int> g(a, 4);
	g.AddEdge("张三", "李四", 100);
	g.AddEdge("张三", "王五", 200);
	g.AddEdge("王五", "赵六", 30);
	g.Print();
}

int main()
{
	TestGraph2();
	return 0;
}

测试结果:

2.2.4 邻接表存储的优缺点

邻接表优点:

● 节约空间:对于稀疏图,邻接表只存储存在的边,节约了空间。
● 方便插入和删除边:插入和删除边的时间复杂度为 O(1) 到 O(V),其中 V 为顶点数,效率较高。

邻接表缺点:

● 查找边的效率较低:查找任意两个顶点间是否相邻的时间复杂度为 O(V),其中 V 为顶点数。
● 不便于图的运算:相比邻接矩阵,邻接表不太适合直观进行图的运算,需要通过遍历链表来查找边的信息。

三、图的遍历

图的边的我们很好遍历,但是我们想要遍历图的节点呢?下面我们就来好好说说

给定一个图G和其中任意一个顶点v0,从v0出发,沿着图中各边访问图中的所有顶点,且每个顶 点仅被遍历一次。"遍历"即对结点进行某种操作的意思。

3.1 图的广度优先遍历(BFS)

广度优先遍历图的意思就是以某一节点为起点,以该节点为中心将路径每次加1,去一层层遍历其周围的节点

下图其A节点为起点画图做的演示:

下面我们开始BFS算法思路的讲解:

基本思路和二叉树的层序遍历是一样的(不熟悉的同学可以看到这里:【LeetCode】二叉树OJ-CSDN博客),用一个队列来保存将要遍历的节点,每次将队列中的一个节点拿出来遍历后,向队列中压入该节点邻接的节点,依次方法一直遍历到队列为空为止

但是我们需要注意的是:由于图的特殊结构,向队列中压入遍历节点临近的节点时,有的临近节点在之前已经被遍历过了,如果不做控制会被重复遍历

如此一来我们需要一个存储容器来标记已经被遍历过的节点即可:

void BFS(const V& src)
{
	queue<int> q;//队列来保存要遍历的节点
	vector<bool> visited(_vertexs.size(), false);//判断对应节点是否有被遍历过
	int n = GetVertexIndex(src);
	if (n == -1)
		return;
	q.push(n);//先将起始节点入队列
	visited[n] = true;//入完队列立即标记,防止重复入队列
	int levelSize = 1;//记录每一层节点的数量,进行分层打印
	while (!q.empty())//遍历队列中的节点
	{
		for (size_t j = 0; j < levelSize; j++)//每次先将每一层的节点打印出来
		{
			int front = q.front();
			cout << _vertexs[front] << " ";
			for (int i = 0; i < _vertexs.size(); ++i)//压入遍历节点的邻近的未被标记的节点
			{
				if (_matrix[front][i] != MAX_W)
				{
					if (visited[i] == false)
					{
						q.push(i);
						visited[i] = true;
					}
				}
			}
			q.pop();
		}
		cout << endl;
		levelSize = q.size();//更新数量为下一层节点数
	}
}

 代码的实现是基于邻接矩阵存储模式下的,下面来测试一下:

void TestGraphBFS()
{
	matrix::Graph<char, int> g1("ABCDEFGHI", 9);
	g1.AddEdge('A', 'B', 1);
	g1.AddEdge('A', 'C', 1);
	g1.AddEdge('A', 'D', 1);
	g1.AddEdge('B', 'E', 1);
	g1.AddEdge('B', 'C', 1);
	g1.AddEdge('C', 'F', 1);
	g1.AddEdge('D', 'F', 1);
	g1.AddEdge('E', 'G', 1);
	g1.AddEdge('F', 'H', 1);
	g1.AddEdge('H', 'I', 1);
	g1.BFS('A');
}

int main()
{
	TestGraphBFS();
	return 0;
}

测试结果:

3.2 图的深度优先遍历(DFS)

深度优先遍历图是以某一节点为起点,每次向后去遍历一个没有被遍历过的邻接节点,如果某个节点的邻接节点都被遍历过了,就沿着遍历的路线回溯到最近的未被遍历的邻接节点,直到图中的所有节点都被遍历完:

对于DFS我们直接使用递归的方法来完成即可(在遍历邻接节点时和BFS一样,我们需要一个存储容器来标记已经被遍历过的节点):

void _DFS(int src, vector<bool>& visited)
{
	cout << _vertexs[src] << endl;
	visited[src] = true;//遍历过后将对应下标值置为真

	//找一个srci相邻的没有访问过的点,去往深度遍历
	for (int i = 0; i < _vertexs.size(); ++i)
	{
		if (_matrix[src][i] != MAX_W && visited[i] == false)
		{
			_DFS(i, visited);
		}
	}
}
void DFS(const V& src)
{
	vector<bool> visited(_vertexs.size(), false);//存储已遍历过的节点
	int n = GetVertexIndex(src);
	if (n != -1)
		_DFS(n, visited);
}

 代码的实现也是基于邻接矩阵存储模式下的,下面来测试一下:

void TestGraphDFS()
{
	matrix::Graph<char, int> g1("ABCDEFGHI", 9);
	g1.AddEdge('A', 'B', 1);
	g1.AddEdge('A', 'C', 1);
	g1.AddEdge('A', 'D', 1);
	g1.AddEdge('B', 'E', 1);
	g1.AddEdge('B', 'C', 1);
	g1.AddEdge('C', 'F', 1);
	g1.AddEdge('D', 'F', 1);
	g1.AddEdge('E', 'G', 1);
	g1.AddEdge('F', 'H', 1);
	g1.AddEdge('H', 'I', 1);
	g1.DFS('A');
}

int main()
{
	TestGraphDFS();
	return 0;
}

测试结果:

3.2 非连通图的遍历 

如果所遍历的图是一个非连通图,那不管用哪种遍历方法不都遍历不了完整的节点了吗?

对于这样的特殊情况,我们在代码实现的过程中都有一个存储容器来标记已经被遍历过的节点,如果该容器中还有未遍历完的节点,直接遍历即可

四、最小生成树

连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树 就不在连通;反之,在其中引入任何一条新边,都会形成一条回路。若连通图由n个顶点组成,则其生成树必含n个顶点和n-1条边。而最小生成树就是所有生成树中权值之和最小的生成树(可以有多个)

因此构造最小生成树的准则有三条:

● 只能使用图中的边来构造最小生成树

● 只能使用恰好n-1条边来连接图中的n个顶点

● 选用的n-1条边不能构成回路

构造最小生成树的方法:Kruskal算法和Prim算法。这两个算法都采用了逐步求解的贪心策略。

贪心算法:是指在问题求解时,总是做出当前看起来最好的选择。也就是说贪心算法做出的不是 整体最优的的选择,而是某种意义上的局部最优解。贪心算法不是对所有的问题都能得到整体最优解。

4.1 Kruskal算法

任给一个有n个顶点的连通网络N={V,E},首先构造一个由这n个顶点组成、不含任何边的图G={V,NULL},其中每个顶点自成一个集合, 其次不断从E中取出权值最小的一条边(若有多条任取其一),若该边的两个顶点来自不同的集合,则将此边加入到G中(若该边的两个顶点来自同一个的集合,此时再将该边添加到G中就构成了回路,不符合最小生成树的要求)。如此重复,直到所有顶点在同一个连通集合上为止。

该算法的核心:每次迭代时,选出一条具有最小权值,且两端点不在同一集合上的边,加入生成树。

下面是画图举例:

那如何判断所查找的到最小权值的边所连接的两端是否在一个集合内呢?

我们可以用并查集来处理(不熟悉的同学可以看到这里:【数据结构高阶】并查集) 

下面我们用代码实现一下Kruskal算法(基于邻接矩阵存储模式):

#include<iostream>
#include<map>
#include<vector>
#include<climits>
#include<queue>

using namespace std;

class UnionFindSet//并查集
{
public:
	UnionFindSet(size_t n)//初始化数据
		:_ufs(n,-1)
	{}

	int FindRoot(int n)//查找元素的根节点
	{
		//查找根节点
		int root = n;
		while (_ufs[root] >= 0)
		{
			root = _ufs[root];
		}
		//路径压缩
		int x = n;
		while (_ufs[x] >= 0)//将路径上所有孩子节点都连接上根节点
		{
			int parent = _ufs[x];
			_ufs[x] = root;
			x = parent;
		}
		return root;
	}

	void Union(int x, int y)//合并两个元素所在树
	{
		int root1 = FindRoot(x);
		int root2 = FindRoot(y);
		if (root1 == root2)//元素所在树都一样就没必要合并了
			return;
		if (abs(_ufs[root1]) < abs(_ufs[root2]))//将数据量少的那个根节点合并到数据量大的根节点上
			swap(root1, root2);
		_ufs[root1] += _ufs[root2];
		_ufs[root2] = root1;
	}

	bool InSet(int x, int y)//判断两个元素是否在同一棵树
	{
		return FindRoot(x) == FindRoot(y);
	}

private:
	vector<int> _ufs;
};

namespace matrix//邻接矩阵存储模式
{
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>//V为顶点类型,W为边的权值类型,MAX_W表示两个顶点到达不了的权值(用户可根据具体情况传入),Direction传入的真假值表示是否为有向图
	class Graph
	{
		typedef Graph<V, W, MAX_W, Direction> Self;

	public:
		Graph() = default;

		Graph(const V* a, size_t n)//传入顶点的集合和顶点数量
		{
			_vertexs.reserve(n);
			for (int i = 0; i < n; ++i)//存储顶点
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;//建立顶点对于邻接矩阵元素下标的映射关系
			}
			_matrix.resize(n);
			for (int i = 0; i < n; ++i)//初始化邻接矩阵
			{
				_matrix[i].resize(n, MAX_W);
			}
		}

		int GetVertexIndex(const V& v)//返回顶点对应的矩阵下标
		{
			auto find = _indexMap.find(v);
			if (find != _indexMap.end())
			{
				return find->second;
			}
			else
			{
				cout << "顶点不存在" << endl;
				return -1;
			}
		}

		void _AddEdge(const int d, const int s, const W& w)
		{
			_matrix[s][d] = w;
			if (Direction == false)//无向图将两端都链接起来
			{
				_matrix[d][s] = w;
			}
		}

		void AddEdge(const V& src, const V& dst, const W& w)
		{
			int s = GetVertexIndex(src);
			int d = GetVertexIndex(dst);
			if (d == s || s == -1 || d == -1)
			{
				cout << "插入的边不合法" << endl;
				return;
			}
			_AddEdge(d, s, w);
		}

		void Print()//图的打印
		{
			cout << "\t";
			for (int i = 0; i < _vertexs.size(); ++i)
			{
				cout << _vertexs[i] << "(" << i << ")" << "\t";
			}
			cout << endl;
			for (int i = 0; i < _matrix.size(); ++i)
			{
				cout << _vertexs[i] << "(" << i << ")" << "\t";
				for (int j = 0; j < _matrix[i].size(); ++j)
				{
					if (_matrix[i][j] == MAX_W)
					{
						cout << "∞\t";
					}
					else
					{
						cout << _matrix[i][j] << "\t";
					}
				}
				cout << endl;
			}
		}

		struct Edge//存储边
		{
			//边两端相邻的节点对应的矩阵下标
			int _srci;
			int _dsti;
			//边的权值
			W _w;

			Edge(const int srci, const int dsti, const W& w)
				:_srci(srci),
				_dsti(dsti),
				_w(w)
			{}

			bool operator>(const Edge& e) const//边的比较
			{
				return _w > e._w;
			}
		};

		W Kruskal(Self& minTree)//minTree是输出型参数,传出最小生成树
		{
			size_t n = _vertexs.size();

			//初始化最小生成树
			minTree._vertexs = _vertexs;
			minTree._indexMap = _indexMap;
			minTree._matrix.resize(n);
			for (int i = 0; i < n; ++i)
			{
				minTree._matrix[i].resize(n, MAX_W);
			}
			priority_queue<Edge, vector<Edge>, greater<Edge>> pq;//建立边的小堆

			//将图中边全部添加到优先级队列(小堆)中
			for (size_t i = 0; i < n; ++i)
			{
				for (size_t j = 0; j < n; ++j)
				{
					if (i < j && _matrix[i][j] != MAX_W)//i<j是防止无向图的边重复被添加
					{
						pq.push(Edge(i, j, _matrix[i][j]));
					}
				}
			}

			//找出n-1条边
			int size = 0;//统计最小生成树的边数
			W totalW = W();//统计最小生成树的总权值
			UnionFindSet ufs(n);//记录顶点与顶点的集合
			while (!pq.empty())
			{
				Edge minv = pq.top();//每次取权值最小的边
				pq.pop();
				if (!ufs.InSet(minv._dsti, minv._srci))//边的两个顶点不在同一集合中
				{
					minTree._AddEdge(minv._dsti, minv._srci, minv._w);//将边添加到最小生成树中
					ufs.Union(minv._dsti, minv._srci);//合并两个顶点所在集合
					totalW += minv._w;
					if (++size == n - 1)//如果选出了n-1条边就直接跳出
						break;
				}
			}
			if (size == n - 1)//找到了最小生成树,返回最小生成树的总权值
			{
				return totalW;
			}
			else//没找到返回默认值
			{
				return W();
			}
		}

	private:
		vector<V> _vertexs;//存储顶点
		map<V, int> _indexMap;//存储顶点所对应的邻接矩阵的下标的映射
		vector<vector<W>> _matrix;//邻接矩阵
	};
}

测试代码:

void TestGraphMinTree()
{
	const char* str = "abcdefghi";
	matrix::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('g', 'i', 6);
	g.AddEdge('h', 'i', 7);
	matrix::Graph<char, int> kminTree;
	cout << "Kruskal:" << g.Kruskal(kminTree) << endl;
	kminTree.Print();
}
int main()
{
	TestGraphMinTree();
	return 0;
}

运行结果:

4.2 Prim算法

Prim算法的工作原理与与Kruskal算法类似。Prim算法所具有的一个性质是集合A中的边总是构成一棵树。如下图所示,从这个图一个任意的结点a添加到A集合中,每一步在连接集合A和A之外的结点的所有边中,选择一条权值最小的边加入到最小生成树中,每次选取的边的另一个不属于A集合的顶点也添加到集合A中,直到所有顶点都添加到A集合中为止:

本算法也属于贪心策略,因为每一步所加入的边都必须是使树的总权重增加量最小的边。相比之下Prim算法是局部的贪心,而Kruskal算法是全局的贪心

我们可以发现Prim算法每次选中的边都不会构成回路,所以在实现时并不需要并查集了

下面我们用代码实现一下Prim算法(基于邻接矩阵存储模式):

#include<iostream>
#include<map>
#include<vector>
#include<climits>
#include<queue>

using namespace std;

namespace matrix//邻接矩阵存储模式
{
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>//V为顶点类型,W为边的权值类型,MAX_W表示两个顶点到达不了的权值(用户可根据具体情况传入),Direction传入的真假值表示是否为有向图
	class Graph
	{
		typedef Graph<V, W, MAX_W, Direction> Self;

	public:
		Graph() = default;

		Graph(const V* a, size_t n)//传入顶点的集合和顶点数量
		{
			_vertexs.reserve(n);
			for (int i = 0; i < n; ++i)//存储顶点
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;//建立顶点对于邻接矩阵元素下标的映射关系
			}
			_matrix.resize(n);
			for (int i = 0; i < n; ++i)//初始化邻接矩阵
			{
				_matrix[i].resize(n, MAX_W);
			}
		}

		int GetVertexIndex(const V& v)//返回顶点对应的矩阵下标
		{
			auto find = _indexMap.find(v);
			if (find != _indexMap.end())
			{
				return find->second;
			}
			else
			{
				cout << "顶点不存在" << endl;
				return -1;
			}
		}

		void _AddEdge(const int d, const int s, const W& w)
		{
			_matrix[s][d] = w;
			if (Direction == false)//无向图将两端都链接起来
			{
				_matrix[d][s] = w;
			}
		}

		void AddEdge(const V& src, const V& dst, const W& w)
		{
			int s = GetVertexIndex(src);
			int d = GetVertexIndex(dst);
			if (d == s || s == -1 || d == -1)
			{
				cout << "插入的边不合法" << endl;
				return;
			}
			_AddEdge(d, s, w);
		}

		void Print()//图的打印
		{

			cout << "\t";
			for (int i = 0; i < _vertexs.size(); ++i)
			{
				cout << _vertexs[i] << "(" << i << ")" << "\t";
			}
			cout << endl;
			for (int i = 0; i < _matrix.size(); ++i)
			{
				cout << _vertexs[i] << "(" << i << ")" << "\t";
				for (int j = 0; j < _matrix[i].size(); ++j)
				{
					if (_matrix[i][j] == MAX_W)
					{
						cout << "∞\t";
					}
					else
					{
						cout << _matrix[i][j] << "\t";
					}
				}
				cout << endl;
			}
		}

		struct Edge//存储边
		{
			//边两端相邻的节点对应的矩阵下标
			int _srci;
			int _dsti;
			//边的权值
			W _w;

			Edge(const int srci, const int dsti, const W& w)
				:_srci(srci),
				_dsti(dsti),
				_w(w)
			{}

			bool operator>(const Edge& e) const//边的比较
			{
				return _w > e._w;
			}
		};

		W Prim(Self& minTree, const V& src)//minTree是输出型参数,传出最小生成树
		{
			size_t n = _vertexs.size();

			//初始化最小生成树
			minTree._vertexs = _vertexs;
			minTree._indexMap = _indexMap;
			minTree._matrix.resize(n);
			for (int i = 0; i < n; ++i)
			{
				minTree._matrix[i].resize(n, MAX_W);
			}

			int srci = GetVertexIndex(src);
			if (srci == -1)
				return W();

			priority_queue<Edge, vector<Edge>, greater<Edge>> pq;//建立边的小堆来记录两个集合之间所连接的边,方便直接选出权值最小的边
			for (int i = 0; i < n; ++i)//先将传入的顶点链接的边存入堆
			{
				if (_matrix[srci][i] != MAX_W)
				{
					pq.push(Edge(srci, i, _matrix[srci][i]));
				}
			}

			vector<bool> A(n, false);//记录被纳入最小生成树的顶点集合A
			A[srci] = true;

			//找出n-1条边
			int size = 0;//统计最小生成树的边数
			W totalW = W();//统计最小生成树的总权值
			while (!pq.empty())
			{
				Edge minv = pq.top();//每次取两个集合链接的权值最小的边
				pq.pop();

				/*由于我们使用priority_queue存储两个集合链接的边来方便我们找到权值最小的边,
					但在不断向最小生成树的添加节点时,两个集合所连接的边会改变,可是priority_queue中的边不会更新,
					这样子如果直接拿堆顶的边添加进最小生成树可能会造成回路的情况*/
				if (A[minv._dsti] == false)//这里判断一下要添加的边的目标顶点不在集合A中再继续添加
				{
					minTree._AddEdge(minv._dsti, minv._srci, minv._w);//将边添加到最小生成树中
					A[minv._dsti] = true;//将添加边的目标顶点纳入集合A中
					totalW += minv._w;
					if (++size == n - 1)
						break;
					for (int i = 0; i < n; ++i)//将新纳入的顶点所能连接到不在A集合中的顶点的边添加到堆中
					{
						if (_matrix[minv._dsti][i] != MAX_W && !A[i])
						{
							pq.push(Edge(minv._dsti, i, _matrix[minv._dsti][i]));
						}
					}
				}
			}
			if (size == n - 1)//找到了最小生成树,返回最小生成树的总权值
			{
				return totalW;
			}
			else//没找到返回默认值
			{
				return W();
			}
		}

	private:
		vector<V> _vertexs;//存储顶点
		map<V, int> _indexMap;//存储顶点所对应的邻接矩阵的下标的映射
		vector<vector<W>> _matrix;//邻接矩阵
	};
}

测试代码:

void TestGraphMinTree()
{
	const char str[] = "abcdefghi";
	matrix::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('g', 'i', 6);
	g.AddEdge('h', 'i', 7);
	matrix::Graph<char, int> pminTree;
	for (int i = 0; i < sizeof(str) / sizeof(char) - 1; ++i)//每次选取不一样的顶点
	{
		cout << "src: " << str[i] << ", Prim:" << g.Prim(pminTree, str[i]) << endl;
		pminTree.Print();
		cout << endl;
	}
}

int main()
{
	TestGraphMinTree();
	return 0;
}

运行结果:

五、最短路径

最短路径就是:从在带权有向图G中的某一顶点出发,找出一条通往另一顶点的最短路径,最短也就是沿路径各边的权值总和达到最小。

5.1 Dijkstra算法

Dijkstra算法就适用于解决带权重的有向图上的单源最短路径问题,同时算法要求图中所有边的权重非负

一般在求解最短路径的时候都是已知一个起点和一个终点,所以使用Dijkstra算法求解过后也就得到了所需起点到终点的最短路径。 针对一个带权有向图G,将所有结点分为两组S和Q,S是已经确定最短路径的结点集合,在初始时为空(初始时就可以将源节点s放入,毕竟源节点到自己的代价是0),Q 为其余未确定最短路径的结点集合,每次从Q中找出一个起点到该结点代价最小的结点u ,将u从Q 中移出,并放入S 中,对u的每一个相邻结点v进行松弛操作。
松弛即对每一个相邻结点v ,判断源节点s到结点u的代价与u到v的代价之和是否比原来s到v的代价更小,若代价比原来小则要将s到v的代价更新为s到u与u到v的代价之和,否则维持原样。如此一直循环直至集合Q为空,即所有节点都已经查找过一遍并确定了最短路径,至于一些起点到达不了的结点在算法循环后其代价仍为初始设定的值,不发生变化。Dijkstra算法每次都是选择V-S中最小的路径节点来进行更新,并加入S中,所以该算法使用的是贪心策略。

下面我们代码实现一下,(基于邻接矩阵存储模式):

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

using namespace std;

namespace matrix//邻接矩阵存储模式
{
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>//V为顶点类型,W为边的权值类型,MAX_W表示两个顶点到达不了的权值(用户可根据具体情况传入),Direction传入的真假值表示是否为有向图
	class Graph
	{
		typedef Graph<V, W, MAX_W, Direction> Self;

	public:
		Graph() = default;

		Graph(const V* a, size_t n)//传入顶点的集合和顶点数量
		{
			_vertexs.reserve(n);
			for (int i = 0; i < n; ++i)//存储顶点
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;//建立顶点对于邻接矩阵元素下标的映射关系
			}
			_matrix.resize(n);
			for (int i = 0; i < n; ++i)//初始化邻接矩阵
			{
				_matrix[i].resize(n, MAX_W);
			}
		}

		int GetVertexIndex(const V& v)//返回顶点对应的矩阵下标
		{
			auto find = _indexMap.find(v);
			if (find != _indexMap.end())
			{
				return find->second;
			}
			else
			{
				cout << v << "顶点不存在" << endl;
				return -1;
			}
		}

		void _AddEdge(const int d, const int s, const W& w)
		{
			_matrix[s][d] = w;
			if (Direction == false)//无向图将两端都链接起来
			{
				_matrix[d][s] = w;
			}
		}

		void AddEdge(const V& src, const V& dst, const W& w)
		{
			int s = GetVertexIndex(src);
			int d = GetVertexIndex(dst);
			if (d == s || s == -1 || d == -1)
			{
				cout << "插入的边不合法" << endl;
				return;
			}
			_AddEdge(d, s, w);
		}

		void PrintShortPath(const V& src, const vector<W>& dist, const vector<int>& parentPath)// 打印最短路径
		{
			int n = _vertexs.size();
			int srci = GetVertexIndex(src);
			for (int i = 0; i < n; ++i)//打印每个顶点从源节点开始的最短路径
			{
				vector<int> path;//存储每个节点对应最短路路径
				int parenti = i;
				while (parenti != srci)
				{
					path.push_back(parenti);
					parenti = parentPath[parenti];
				}
				path.push_back(srci);
				auto p = path.crbegin();
				cout << "[" << _vertexs[i] << "]:";
				while (p != path.crend())//由于path存储的路径是逆着的,我们倒着打印
				{
					if (p != path.crend() - 1)
						cout << _vertexs[*p] << "--" << _matrix[*p][*(p + 1)] << "-->";
					else
						cout << _vertexs[*p] << "  totalW:" << dist[i];
					p++;
				}
				cout << endl;
			}
		}

		void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath)//src为传入的源节点,dist为保存从源节点到各个节点下标对应的代价,pPath记录所生成最短路径上每个节点对应的前一个节点
		{
			int n = _vertexs.size();
			int srci = GetVertexIndex(src);
			pPath.resize(n, -1);
			pPath[srci] = srci;
			dist.resize(n, MAX_W);
			dist[srci] = 0;
			vector<bool> S(n, false);//集合S,记录确定最短路径节点的集合
			
			for (int i = 0; i < n; ++i)
			{
				//选出不在S集合中的代价最小的节点
				int u = 0;//记录节点下标
				W min = MAX_W;
				for (int j = 0; j < n; ++j)
				{
					if (S[j] == false && dist[j] < min)
					{
						min = dist[j];
						u = j;
					}
				}

				//松弛更新
				for (int j = 0; j < n; ++j)
				{
					if (S[j] == false && _matrix[u][j] != MAX_W && _matrix[u][j] + dist[u] < dist[j])//节点相邻,并且更新的节点不在集合S中,并且新的代价小于原代价
					{
						dist[j] = _matrix[u][j] + dist[u];
						pPath[j] = u;
					}
				}
				S[u] = true;//最后将找到的代价最小的节点纳入到集合S中
			}
		}

	private:
		vector<V> _vertexs;//存储顶点
		map<V, int> _indexMap;//存储顶点所对应的邻接矩阵的下标的映射
		vector<vector<W>> _matrix;//邻接矩阵
	};
}

测试代码:

void TestGraphDijkstra()
{
	const char* str = "syztx";
	matrix::Graph<char, int, INT_MAX, true> g(str, strlen(str));
	g.AddEdge('s', 't', 10);
	g.AddEdge('s', 'y', 5);
	g.AddEdge('y', 't', 3);
	g.AddEdge('y', 'x', 9);
	g.AddEdge('y', 'z', 2);
	g.AddEdge('z', 's', 7);
	g.AddEdge('z', 'x', 6);
	g.AddEdge('t', 'y', 2);
	g.AddEdge('t', 'x', 1);
	g.AddEdge('x', 'z', 4);
	vector<int> dist;//源节点到下标对应节点最短路径的总权值(代价)
	vector<int> parentPath;//存储最短路径每个节点的前一个节点
	g.Dijkstra('s', dist, parentPath);
	g.PrintShortPath('s', dist, parentPath);
	
	// 图中带有负权路径时,贪心策略则失效了。
	// 测试结果可以看到s->t->y之间的最短路径没更新出来
	cout << endl << "下面是带有负权的图" << endl;
	const char* str2 = "sytx";
	matrix::Graph<char, int, INT_MAX, true> g2(str2, strlen(str2));
	g2.AddEdge('s', 't', 10);
	g2.AddEdge('s', 'y', 5);
	g2.AddEdge('t', 'y', -7);
	g2.AddEdge('y', 'x', 3);
	vector<int> dist2;
	vector<int> parentPath2;
	g2.Dijkstra('s', dist2, parentPath2);
	g2.PrintShortPath('s', dist2, parentPath2);
}

int main()
{
	TestGraphDijkstra();
	return 0;
}

测试结果:

通过测试,我们也可以发现:Dijkstra算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路径的最短路径。 

该算法的时间复杂的度为O(N²)

5.2 Bellman-Ford算法

Dijkstra算法只能用来解决正权图的单源最短路径问题,但有时目会出现负权图。这时这个算法就不能帮助我们解决问题了,而Bellman-Ford算法可以解决负权图的单源最短路径问题。

Bellman-Ford算法的优点是可以解决有负权边的单源最短路径问题,而且可以用来判断是否有负权回路。但它也有明显的缺点:在邻接表的存储模式下,它的时间复杂度 O(N*E) (N是点数,E是边数)普遍是要高于Dijkstra算法O(N²)的;在邻接矩阵的存储模式下,那么遍历所有边的数量的时间复杂度就是O(N^3),这里也可以看出来Bellman-Ford就是一种暴力求解更新。

下面直接上代码看思路:

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

using namespace std;

namespace matrix//邻接矩阵存储模式
{
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>//V为顶点类型,W为边的权值类型,MAX_W表示两个顶点到达不了的权值(用户可根据具体情况传入),Direction传入的真假值表示是否为有向图
	class Graph
	{
		typedef Graph<V, W, MAX_W, Direction> Self;

	public:
		Graph() = default;

		Graph(const V* a, size_t n)//传入顶点的集合和顶点数量
		{
			_vertexs.reserve(n);
			for (int i = 0; i < n; ++i)//存储顶点
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;//建立顶点对于邻接矩阵元素下标的映射关系
			}
			_matrix.resize(n);
			for (int i = 0; i < n; ++i)//初始化邻接矩阵
			{
				_matrix[i].resize(n, MAX_W);
			}
		}

		int GetVertexIndex(const V& v)//返回顶点对应的矩阵下标
		{
			auto find = _indexMap.find(v);
			if (find != _indexMap.end())
			{
				return find->second;
			}
			else
			{
				cout << v << "顶点不存在" << endl;
				return -1;
			}
		}

		void _AddEdge(const int d, const int s, const W& w)
		{
			_matrix[s][d] = w;
			if (Direction == false)//无向图将两端都链接起来
			{
				_matrix[d][s] = w;
			}
		}

		void AddEdge(const V& src, const V& dst, const W& w)
		{
			int s = GetVertexIndex(src);
			int d = GetVertexIndex(dst);
			if (d == s || s == -1 || d == -1)
			{
				cout << "插入的边不合法" << endl;
				return;
			}
			_AddEdge(d, s, w);
		}

		void PrintShortPath(const V& src, const vector<W>& dist, const vector<int>& parentPath)// 打印最短路径
		{
			int n = _vertexs.size();
			int srci = GetVertexIndex(src);
			for (int i = 0; i < n; ++i)//打印每个顶点从源节点开始的最短路径
			{
				vector<int> path;//存储每个节点对应最短路路径
				int parenti = i;
				while (parenti != srci)
				{
					path.push_back(parenti);
					parenti = parentPath[parenti];
				}
				path.push_back(srci);
				auto p = path.crbegin();
				cout << "[" << _vertexs[i] << "]:";
				while (p != path.crend())//由于path存储的路径是逆着的,我们倒着打印
				{
					if (p != path.crend() - 1)
						cout << _vertexs[*p] << "--(" << _matrix[*p][*(p + 1)] << ")-->";
					else
						cout << _vertexs[*p] << "  totalW:" << dist[i];
					p++;
				}
				cout << endl;
			}
		}

		void BellmanFord(const V& src, vector<W>& dist, vector<int>& pPath)//src为传入的源节点,dist为保存从源节点到各个节点下标对应的代价,pPath记录所生成最短路径上每个节点对应的前一个节点
		{
			int n = _vertexs.size();
			int srci = GetVertexIndex(src);
			pPath.resize(n, -1);
			pPath[srci] = srci;
			dist.resize(n, MAX_W);
			dist[srci] = 0;

			//遍历图中的边
			for (int i = 0; i < n; ++i)
			{
				for (int j = 0; j < n; ++j)
				{
					if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])//如果顶点之间相连,且源节点到i顶点再到j顶点的代价小于当前dist中保存的从源节点到j顶点的代价就行更新
					{
						dist[j] = dist[i] + _matrix[i][j];//更新代价
						pPath[j] = i;//更新j在最短路径中前一个节点
					}
				}
			}
		}

	private:
		vector<V> _vertexs;//存储顶点
		map<V, int> _indexMap;//存储顶点所对应的邻接矩阵的下标的映射
		vector<vector<W>> _matrix;//邻接矩阵
	};
}

我们可以看到该算法的核心思路就是遍历所有的边,如果遍历到的边:源节点到该边的起点再到该边的终点的代价,小于当前dist中保存的从源节点到该边的终点的代价就行更新

下面测试一下:

void TestGraphBellmanFord()
{
	const char* str = "syztx";
	matrix::Graph<char, int, INT_MAX, true> g(str, strlen(str));
	g.AddEdge('s', 't', 6);
	g.AddEdge('s', 'y', 7);
	g.AddEdge('y', 'z', 9);
	g.AddEdge('y', 'x', -3);
	g.AddEdge('z', 's', 2);
	g.AddEdge('z', 'x', 7);
	g.AddEdge('t', 'x', 5);
	g.AddEdge('t', 'y', 8);
	g.AddEdge('t', 'z', -4);
	g.AddEdge('x', 't', -2);
	vector<int> dist;
	vector<int> parentPath;
	g.BellmanFord('s', dist, parentPath);
	g.PrintShortPath('s', dist, parentPath);
}

int main()
{
	TestGraphBellmanFord();
	return 0;
}

测试的图结构:

测试结果:

咦??结果不对啊,z节点的最短路径怎么会是2? 计算怎么出了错误呢?

我们细细分析一下会发现按照上面的算法在更新x->t代价为-2时,x->y->z的代价并没有得到更新,所以我们只遍历一遍图中的节点时,更新某节点的代价会影响到该节点最短路径之前的节点,但是并没有得到修正

下面我们将代码改进一下,将图中的节点遍历n(节点的个数)遍:

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

using namespace std;

namespace matrix//邻接矩阵存储模式
{
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>//V为顶点类型,W为边的权值类型,MAX_W表示两个顶点到达不了的权值(用户可根据具体情况传入),Direction传入的真假值表示是否为有向图
	class Graph
	{
		typedef Graph<V, W, MAX_W, Direction> Self;

	public:
		Graph() = default;

		Graph(const V* a, size_t n)//传入顶点的集合和顶点数量
		{
			_vertexs.reserve(n);
			for (int i = 0; i < n; ++i)//存储顶点
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;//建立顶点对于邻接矩阵元素下标的映射关系
			}
			_matrix.resize(n);
			for (int i = 0; i < n; ++i)//初始化邻接矩阵
			{
				_matrix[i].resize(n, MAX_W);
			}
		}

		int GetVertexIndex(const V& v)//返回顶点对应的矩阵下标
		{
			auto find = _indexMap.find(v);
			if (find != _indexMap.end())
			{
				return find->second;
			}
			else
			{
				cout << v << "顶点不存在" << endl;
				return -1;
			}
		}

		void _AddEdge(const int d, const int s, const W& w)
		{
			_matrix[s][d] = w;
			if (Direction == false)//无向图将两端都链接起来
			{
				_matrix[d][s] = w;
			}
		}

		void AddEdge(const V& src, const V& dst, const W& w)
		{
			int s = GetVertexIndex(src);
			int d = GetVertexIndex(dst);
			if (d == s || s == -1 || d == -1)
			{
				cout << "插入的边不合法" << endl;
				return;
			}
			_AddEdge(d, s, w);
		}

		void PrintShortPath(const V& src, const vector<W>& dist, const vector<int>& parentPath)// 打印最短路径
		{
			int n = _vertexs.size();
			int srci = GetVertexIndex(src);
			for (int i = 0; i < n; ++i)//打印每个顶点从源节点开始的最短路径
			{
				vector<int> path;//存储每个节点对应最短路路径
				int parenti = i;
				while (parenti != srci)
				{
					path.push_back(parenti);
					parenti = parentPath[parenti];
				}
				path.push_back(srci);
				auto p = path.crbegin();
				cout << "[" << _vertexs[i] << "]:";
				while (p != path.crend())//由于path存储的路径是逆着的,我们倒着打印
				{
					if (p != path.crend() - 1)
						cout << _vertexs[*p] << "--(" << _matrix[*p][*(p + 1)] << ")-->";
					else
						cout << _vertexs[*p] << "  totalW:" << dist[i];
					p++;
				}
				cout << endl;
			}
		}

		void BellmanFord(const V& src, vector<W>& dist, vector<int>& pPath)//src为传入的源节点,dist为保存从源节点到各个节点下标对应的代价,pPath记录所生成最短路径上每个节点对应的前一个节点
		{
			int n = _vertexs.size();
			int srci = GetVertexIndex(src);
			pPath.resize(n, -1);
			pPath[srci] = srci;
			dist.resize(n, MAX_W);
			dist[srci] = 0;

			for (int k = 0; k < n; ++k)//更新n次
			{
				bool update = false;
				//遍历图中的边
				for (int i = 0; i < n; ++i)
				{
					for (int j = 0; j < n; ++j)
					{
						if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])//如果顶点之间相连,且源节点到i顶点再到j顶点的代价小于当前dist中保存的从源节点到j顶点的代价就行更新
						{
							dist[j] = dist[i] + _matrix[i][j];//更新代价
							pPath[j] = i;//更新j在最短路径中前一个节点
							update = true;
						}
					}
				}
				if (update == false)//如果没有发生更新直接跳出
				{
					break;
				}
			}
		}

	private:
		vector<V> _vertexs;//存储顶点
		map<V, int> _indexMap;//存储顶点所对应的邻接矩阵的下标的映射
		vector<vector<W>> _matrix;//邻接矩阵
	};
}

假设每个顶点的代价都要进行修正,最多修正节点数次即可

再来进行测试:

结果正确

但是Bellman-Ford算法有一个缺陷,所计算的图中不能带有负权回路,否则在算法内部会一直进行更新修正

下面最后修正一下代码,让其遇到带有负权回路图时返回错误:

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

using namespace std;

namespace matrix//邻接矩阵存储模式
{
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>//V为顶点类型,W为边的权值类型,MAX_W表示两个顶点到达不了的权值(用户可根据具体情况传入),Direction传入的真假值表示是否为有向图
	class Graph
	{
		typedef Graph<V, W, MAX_W, Direction> Self;

	public:
		Graph() = default;

		Graph(const V* a, size_t n)//传入顶点的集合和顶点数量
		{
			_vertexs.reserve(n);
			for (int i = 0; i < n; ++i)//存储顶点
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;//建立顶点对于邻接矩阵元素下标的映射关系
			}
			_matrix.resize(n);
			for (int i = 0; i < n; ++i)//初始化邻接矩阵
			{
				_matrix[i].resize(n, MAX_W);
			}
		}

		int GetVertexIndex(const V& v)//返回顶点对应的矩阵下标
		{
			auto find = _indexMap.find(v);
			if (find != _indexMap.end())
			{
				return find->second;
			}
			else
			{
				cout << v << "顶点不存在" << endl;
				return -1;
			}
		}

		void _AddEdge(const int d, const int s, const W& w)
		{
			_matrix[s][d] = w;
			if (Direction == false)//无向图将两端都链接起来
			{
				_matrix[d][s] = w;
			}
		}

		void AddEdge(const V& src, const V& dst, const W& w)
		{
			int s = GetVertexIndex(src);
			int d = GetVertexIndex(dst);
			if (d == s || s == -1 || d == -1)
			{
				cout << "插入的边不合法" << endl;
				return;
			}
			_AddEdge(d, s, w);
		}

		void PrintShortPath(const V& src, const vector<W>& dist, const vector<int>& parentPath)// 打印最短路径
		{
			int n = _vertexs.size();
			int srci = GetVertexIndex(src);
			for (int i = 0; i < n; ++i)//打印每个顶点从源节点开始的最短路径
			{
				vector<int> path;//存储每个节点对应最短路路径
				int parenti = i;
				while (parenti != srci)
				{
					path.push_back(parenti);
					parenti = parentPath[parenti];
				}
				path.push_back(srci);
				auto p = path.crbegin();
				cout << "[" << _vertexs[i] << "]:";
				while (p != path.crend())//由于path存储的路径是逆着的,我们倒着打印
				{
					if (p != path.crend() - 1)
						cout << _vertexs[*p] << "--(" << _matrix[*p][*(p + 1)] << ")-->";
					else
						cout << _vertexs[*p] << "  totalW:" << dist[i];
					p++;
				}
				cout << endl;
			}
		}

		void BellmanFord(const V& src, vector<W>& dist, vector<int>& pPath)//src为传入的源节点,dist为保存从源节点到各个节点下标对应的代价,pPath记录所生成最短路径上每个节点对应的前一个节点
		{
			int n = _vertexs.size();
			int srci = GetVertexIndex(src);
			pPath.resize(n, -1);
			pPath[srci] = srci;
			dist.resize(n, MAX_W);
			dist[srci] = 0;

			for (int k = 0; k < n; ++k)//更新n次
			{
				bool update = false;
				//遍历图中的边
				for (int i = 0; i < n; ++i)
				{
					for (int j = 0; j < n; ++j)
					{
						if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])//如果顶点之间相连,且源节点到i顶点再到j顶点的代价小于当前dist中保存的从源节点到j顶点的代价就行更新
						{
							dist[j] = dist[i] + _matrix[i][j];//更新代价
							pPath[j] = i;//更新j在最短路径中前一个节点
							update = true;
						}
					}
				}
				if (update == false)//如果没有发生更新直接跳出
				{
					break;
				}
			}

			bool update = false;
			//再更新一次,如果图带有负权回路,会继续进行更新修正
			for (int i = 0; i < n; ++i)
			{
				for (int j = 0; j < n; ++j)
				{
					if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])//如果顶点之间相连,且源节点到i顶点再到j顶点的代价小于当前dist中保存的从源节点到j顶点的代价就行更新
					{
						dist[j] = dist[i] + _matrix[i][j];//更新代价
						pPath[j] = i;//更新j在最短路径中前一个节点
						update = true;
					}
				}
			}

			return !update;
		}

	private:
		vector<V> _vertexs;//存储顶点
		map<V, int> _indexMap;//存储顶点所对应的邻接矩阵的下标的映射
		vector<vector<W>> _matrix;//邻接矩阵
	};
}

5.3 Floyd-Warshall算法

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}取得的一条最短路径。

即Floyd算法本质是三维动态规划,D[i][j][k]表示从点i到点j只经过0到k个点最短路径,然后建立起转移方程,然后通过空间优化,优化掉最后一维度,变成一个最短路径的迭代算法,最后即得到所有点的最短路。(其实在Dijkstra和Bellman-Ford算法也采用了降维的思想,我们在算法中使用一维数组dist和parentPath来分别表示两个点的代价和前驱关系)

下面是使用Floyd-Warshall算法查找下图中任意两点间的最短路径的过程(D二维数组存储两个点之间的权值,II二维数组存储两个点所构成的最短路径纵坐标所连接的上一个节点):

下面用代码实现一下:

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

using namespace std;

namespace matrix//邻接矩阵存储模式
{
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>//V为顶点类型,W为边的权值类型,MAX_W表示两个顶点到达不了的权值(用户可根据具体情况传入),Direction传入的真假值表示是否为有向图
	class Graph
	{
		typedef Graph<V, W, MAX_W, Direction> Self;

	public:
		Graph() = default;

		Graph(const V* a, size_t n)//传入顶点的集合和顶点数量
		{
			_vertexs.reserve(n);
			for (int i = 0; i < n; ++i)//存储顶点
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;//建立顶点对于邻接矩阵元素下标的映射关系
			}
			_matrix.resize(n);
			for (int i = 0; i < n; ++i)//初始化邻接矩阵
			{
				_matrix[i].resize(n, MAX_W);
			}
		}

		int GetVertexIndex(const V& v)//返回顶点对应的矩阵下标
		{
			auto find = _indexMap.find(v);
			if (find != _indexMap.end())
			{
				return find->second;
			}
			else
			{
				cout << v << "顶点不存在" << endl;
				return -1;
			}
		}

		void _AddEdge(const int d, const int s, const W& w)
		{
			_matrix[s][d] = w;
			if (Direction == false)//无向图将两端都链接起来
			{
				_matrix[d][s] = w;
			}
		}

		void AddEdge(const V& src, const V& dst, const W& w)
		{
			int s = GetVertexIndex(src);
			int d = GetVertexIndex(dst);
			if (d == s || s == -1 || d == -1)
			{
				cout << "插入的边不合法" << endl;
				return;
			}
			_AddEdge(d, s, w);
		}

		void PrintShortPath(const V& src, const vector<W>& dist, const vector<int>& parentPath)// 打印最短路径
		{
			int n = _vertexs.size();
			int srci = GetVertexIndex(src);
			for (int i = 0; i < n; ++i)//打印每个顶点从源节点开始的最短路径
			{
				vector<int> path;//存储每个节点对应最短路路径
				int parenti = i;
				while (parenti != srci)
				{
					path.push_back(parenti);
					parenti = parentPath[parenti];
				}
				path.push_back(srci);
				auto p = path.crbegin();
				cout << "[" << _vertexs[i] << "]:";
				while (p != path.crend())//由于path存储的路径是逆着的,我们倒着打印
				{
					if (p != path.crend() - 1)
						cout << _vertexs[*p] << "--(" << _matrix[*p][*(p + 1)] << ")-->";
					else
						cout << _vertexs[*p] << "  totalW:" << dist[i];
					p++;
				}
				cout << endl;
			}
		}

		void FloydWarShall(vector<vector<W>>& vvDist, vector<vector<int>>& vvParentPath)//vvDist保存从各个节点到各个节点下标对应的代价, vvParentPath记录各个源节点所生成最短路径上每个节点对应的前一个节点
		{
			//初始化vvDist和vvParentPath
			int n = _vertexs.size();
			vvDist.resize(n);
			vvParentPath.resize(n);
			for (int i = 0; i < n; ++i)
			{
				vvDist[i].resize(n, MAX_W);
				vvParentPath[i].resize(n, -1);
				for (int j = 0; j < n; ++j)
				{
					if (_matrix[i][j] != MAX_W)
					{
						vvDist[i][j] = _matrix[i][j];
						vvParentPath[i][j] = i;
					}
					if (i == j)
					{
						vvDist[i][j] = W();
					}
				}
			}

			//更新vvDist和vvParentPath
			for (int k = 0; k < n; ++k)
			{
				for (int i = 0; i < n; ++i)
				{
					for (int j = 0; j < n; ++j)
					{
						//i->j两个节点路径之间经过k节点
						if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W && vvDist[i][k] + vvDist[k][j] < vvDist[i][j])//经过k节点的路径比原路径代价小
						{
							vvDist[i][j] = vvDist[i][k] + vvDist[k][j];//更新i->j的代价

							//更新源节点i到j最短路径的j前一个节点为源节点k到j的前节点(k节点到j节点之间可能会存在多个节点)
							//如果k->j直接相连,上一个点就k, vvpPath[k][j]存就是k
							//如果k->j没有直接相连,k->..->x->j, vpPath[k][j]存就是x
							vvParentPath[i][j] = vvParentPath[k][j];
						}
					}
				}
			}
		}

	private:
		vector<V> _vertexs;//存储顶点
		map<V, int> _indexMap;//存储顶点所对应的邻接矩阵的下标的映射
		vector<vector<W>> _matrix;//邻接矩阵
	};
}

测试一下:

void TestFloydWarShall()
{
	const char* str = "12345";
	matrix::Graph<char, int, INT_MAX, true> g(str, strlen(str));
	g.AddEdge('1', '2', 3);
	g.AddEdge('1', '3', 8);
	g.AddEdge('1', '5', -4);
	g.AddEdge('2', '4', 1);
	g.AddEdge('2', '5', 7);
	g.AddEdge('3', '2', 4);
	g.AddEdge('4', '1', 2);
	g.AddEdge('4', '3', -5);
	g.AddEdge('5', '4', 6);
	vector<vector<int>> vvDist;
	vector<vector<int>> vvParentPath;
	g.FloydWarShall(vvDist, vvParentPath);
	// 打印任意两点之间的最短路径
	for (size_t i = 0; i < strlen(str); ++i)
	{
		g.PrintShortPath(str[i], vvDist[i], vvParentPath[i]);
		cout << endl;
	}
}

int main()
{
	TestFloydWarShall();
	return 0;
}

六、图的完整代码

6.1 实现代码

#pragma once
#include<iostream>
#include<map>
#include<vector>
#include<climits>
#include<queue>

using namespace std;

class UnionFindSet//并查集
{
public:
	UnionFindSet(size_t n)//初始化数据
		:_ufs(n,-1)
	{}

	int FindRoot(int n)//查找元素的根节点
	{
		//查找根节点
		int root = n;
		while (_ufs[root] >= 0)
		{
			root = _ufs[root];
		}
		//路径压缩
		int x = n;
		while (_ufs[x] >= 0)//将路径上所有孩子节点都连接上根节点
		{
			int parent = _ufs[x];
			_ufs[x] = root;
			x = parent;
		}
		return root;
	}

	void Union(int x, int y)//合并两个元素所在树
	{
		int root1 = FindRoot(x);
		int root2 = FindRoot(y);
		if (root1 == root2)//元素所在树都一样就没必要合并了
			return;
		if (abs(_ufs[root1]) < abs(_ufs[root2]))//将数据量少的那个根节点合并到数据量大的根节点上
			swap(root1, root2);
		_ufs[root1] += _ufs[root2];
		_ufs[root2] = root1;
	}

	bool InSet(int x, int y)//判断两个元素是否在同一棵树
	{
		return FindRoot(x) == FindRoot(y);
	}

	size_t SetSize()//返回并查集中树的个数
	{
		size_t size = 0;
		for (auto e : _ufs)
		{
			if (e < 0)
				++size;
		}
		return size;
	}

private:
	vector<int> _ufs;
};


namespace matrix//邻接矩阵存储模式
{
	template<class V, class W, W MAX_W = INT_MAX, bool Direction = false>//V为顶点类型,W为边的权值类型,MAX_W表示两个顶点到达不了的权值(用户可根据具体情况传入),Direction传入的真假值表示是否为有向图
	class Graph
	{
		typedef Graph<V, W, MAX_W, Direction> Self;

	public:
		Graph() = default;

		Graph(const V* a, size_t n)//传入顶点的集合和顶点数量
		{
			_vertexs.reserve(n);
			for (int i = 0; i < n; ++i)//存储顶点
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;//建立顶点对于邻接矩阵元素下标的映射关系
			}
			_matrix.resize(n);
			for (int i = 0; i < n; ++i)//初始化邻接矩阵
			{
				_matrix[i].resize(n, MAX_W);
			}
		}

		int GetVertexIndex(const V& v)//返回顶点对应的矩阵下标
		{
			auto find = _indexMap.find(v);
			if (find != _indexMap.end())
			{
				return find->second;
			}
			else
			{
				cout << v << "顶点不存在" << endl;
				return -1;
			}
		}

		void _AddEdge(const int d, const int s, const W& w)
		{
			_matrix[s][d] = w;
			if (Direction == false)//无向图将两端都链接起来
			{
				_matrix[d][s] = w;
			}
		}

		void AddEdge(const V& src, const V& dst, const W& w)
		{
			int s = GetVertexIndex(src);
			int d = GetVertexIndex(dst);
			if (d == s || s == -1 || d == -1)
			{
				cout << "插入的边不合法" << endl;
				return;
			}
			_AddEdge(d, s, w);
		}

		void Print()//图的打印
		{

			cout << "\t";
			for (int i = 0; i < _vertexs.size(); ++i)
			{
				cout << _vertexs[i] << "(" << i << ")" << "\t";
			}
			cout << endl;
			for (int i = 0; i < _matrix.size(); ++i)
			{
				cout << _vertexs[i] << "(" << i << ")" << "\t";
				for (int j = 0; j < _matrix[i].size(); ++j)
				{
					if (_matrix[i][j] == MAX_W)
					{
						cout << "∞\t";
					}
					else
					{
						cout << _matrix[i][j] << "\t";
					}
				}
				cout << endl;
			}
		}

		void BFS(const V& src)
		{
			queue<int> q;//队列来保存要遍历的节点
			vector<bool> visited(_vertexs.size(), false);//判断对应节点是否有被遍历过
			int n = GetVertexIndex(src);
			if (n == -1)
				return;
			q.push(n);//先将起始节点入队列
			visited[n] = true;//入完队列立即标记,防止重复入队列
			int levelSize = 1;//记录每一层节点的数量,进行分层打印
			while (!q.empty())//遍历队列中的节点
			{
				for (size_t j = 0; j < levelSize; j++)//每次先将每一层的节点打印出来
				{
					int front = q.front();
					cout << _vertexs[front] << " ";
					for (int i = 0; i < _vertexs.size(); ++i)//压入遍历节点的邻近的未被标记的节点
					{
						if (_matrix[front][i] != MAX_W)
						{
							if (visited[i] == false)
							{
								q.push(i);
								visited[i] = true;
							}
						}
					}
					q.pop();
				}
				cout << endl;
				levelSize = q.size();//更新数量为下一层节点数
			}
		}

		void _DFS(int src, vector<bool>& visited)
		{
			cout << _vertexs[src] << endl;
			visited[src] = true;//遍历过后将对应下标值置为真

			//找一个srci相邻的没有访问过的点,去往深度遍历
			for (int i = 0; i < _vertexs.size(); ++i)
			{
				if (_matrix[src][i] != MAX_W && visited[i] == false)
				{
					_DFS(i, visited);
				}
			}
		}
		void DFS(const V& src)
		{
			vector<bool> visited(_vertexs.size(), false);//存储已遍历过的节点
			int n = GetVertexIndex(src);
			if (n != -1)
				_DFS(n, visited);
		}

		struct Edge//存储边
		{
			//边两端相邻的节点对应的矩阵下标
			int _srci;
			int _dsti;
			//边的权值
			W _w;

			Edge(const int srci, const int dsti, const W& w)
				:_srci(srci),
				_dsti(dsti),
				_w(w)
			{}

			bool operator>(const Edge& e) const//边的比较
			{
				return _w > e._w;
			}
		};

		W Kruskal(Self& minTree)//minTree是输出型参数,传出最小生成树
		{
			size_t n = _vertexs.size();

			//初始化最小生成树
			minTree._vertexs = _vertexs;
			minTree._indexMap = _indexMap;
			minTree._matrix.resize(n);
			for (int i = 0; i < n; ++i)
			{
				minTree._matrix[i].resize(n, MAX_W);
			}
			priority_queue<Edge, vector<Edge>, greater<Edge>> pq;//建立边的小堆

			//将图中边全部添加到优先级队列(小堆)中
			for (size_t i = 0; i < n; ++i)
			{
				for (size_t j = 0; j < n; ++j)
				{
					if (i < j && _matrix[i][j] != MAX_W)//i<j是防止无向图的边重复被添加
					{
						pq.push(Edge(i, j, _matrix[i][j]));
					}
				}
			}

			//找出n-1条边
			int size = 0;//统计最小生成树的边数
			W totalW = W();//统计最小生成树的总权值
			UnionFindSet ufs(n);//记录顶点与顶点的集合
			while (!pq.empty())
			{
				Edge minv = pq.top();//每次取权值最小的边
				pq.pop();
				if (!ufs.InSet(minv._dsti, minv._srci))//边的两个顶点不在同一集合中
				{
					minTree._AddEdge(minv._dsti, minv._srci, minv._w);//将边添加到最小生成树中
					ufs.Union(minv._dsti, minv._srci);//合并两个顶点所在集合
					totalW += minv._w;
					if (++size == n - 1)//如果选出了n-1条边就直接跳出
						break;
				}
			}
			if (size == n - 1)//找到了最小生成树,返回最小生成树的总权值
			{
				return totalW;
			}
			else//没找到返回默认值
			{
				return W();
			}
		}

		W Prim(Self& minTree, const V& src)//minTree是输出型参数,传出最小生成树
		{
			size_t n = _vertexs.size();

			//初始化最小生成树
			minTree._vertexs = _vertexs;
			minTree._indexMap = _indexMap;
			minTree._matrix.resize(n);
			for (int i = 0; i < n; ++i)
			{
				minTree._matrix[i].resize(n, MAX_W);
			}

			int srci = GetVertexIndex(src);
			if (srci == -1)
				return W();

			priority_queue<Edge, vector<Edge>, greater<Edge>> pq;//建立边的小堆来记录两个集合之间所连接的边,方便直接选出权值最小的边
			for (int i = 0; i < n; ++i)//先将传入的顶点链接的边存入堆
			{
				if (_matrix[srci][i] != MAX_W)
				{
					pq.push(Edge(srci, i, _matrix[srci][i]));
				}
			}

			vector<bool> A(n, false);//记录被纳入最小生成树的顶点集合A
			A[srci] = true;

			//找出n-1条边
			int size = 0;//统计最小生成树的边数
			W totalW = W();//统计最小生成树的总权值
			while (!pq.empty())
			{
				Edge minv = pq.top();//每次取两个集合链接的权值最小的边
				pq.pop();

				/*由于我们使用priority_queue存储两个集合链接的边来方便我们找到权值最小的边,
					但在不断向最小生成树的添加节点时,两个集合所连接的边会改变,可是priority_queue中的边不会更新,
					这样子如果直接拿堆顶的边添加进最小生成树可能会造成回路的情况*/
				if (A[minv._dsti] == false)//这里判断一下要添加的边的目标顶点不在集合A中再继续添加
				{
					minTree._AddEdge(minv._dsti, minv._srci, minv._w);//将边添加到最小生成树中
					A[minv._dsti] = true;//将添加边的目标顶点纳入集合A中
					totalW += minv._w;
					if (++size == n - 1)
						break;
					for (int i = 0; i < n; ++i)//将新纳入的顶点所能连接到不在A集合中的顶点的边添加到堆中
					{
						if (_matrix[minv._dsti][i] != MAX_W && !A[i])
						{
							pq.push(Edge(minv._dsti, i, _matrix[minv._dsti][i]));
						}
					}
				}
			}
			if (size == n - 1)//找到了最小生成树,返回最小生成树的总权值
			{
				return totalW;
			}
			else//没找到返回默认值
			{
				return W();
			}
		}

		void PrintShortPath(const V& src, const vector<W>& dist, const vector<int>& parentPath)// 打印最短路径
		{
			int n = _vertexs.size();
			int srci = GetVertexIndex(src);
			for (int i = 0; i < n; ++i)//打印每个顶点从源节点开始的最短路径
			{
				vector<int> path;//存储每个节点对应最短路路径
				int parenti = i;
				while (parenti != srci)
				{
					path.push_back(parenti);
					parenti = parentPath[parenti];
				}
				path.push_back(srci);
				auto p = path.crbegin();
				cout << "[" << _vertexs[i] << "]:";
				while (p != path.crend())//由于path存储的路径是逆着的,我们倒着打印
				{
					if (p != path.crend() - 1)
						cout << _vertexs[*p] << "--(" << _matrix[*p][*(p + 1)] << ")-->";
					else
						cout << _vertexs[*p] << "  totalW:" << dist[i];
					p++;
				}
				cout << endl;
			}
		}

		void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath)//src为传入的源节点,dist为保存从源节点到各个节点下标对应的代价,pPath记录所生成最短路径上每个节点对应的前一个节点
		{
			int n = _vertexs.size();
			int srci = GetVertexIndex(src);
			pPath.resize(n, -1);
			pPath[srci] = srci;
			dist.resize(n, MAX_W);
			dist[srci] = W();
			vector<bool> S(n, false);//集合S,记录确定最短路径节点的集合

			for (int i = 0; i < n; ++i)
			{
				//选出不在S集合中的代价最小的节点
				int u = 0;//记录节点下标
				W min = MAX_W;
				for (int j = 0; j < n; ++j)
				{
					if (S[j] == false && dist[j] < min)
					{
						min = dist[j];
						u = j;
					}
				}

				//松弛更新
				for (int j = 0; j < n; ++j)
				{
					if (S[j] == false && _matrix[u][j] != MAX_W && _matrix[u][j] + dist[u] < dist[j])//节点相邻,并且更新的节点不在集合S中,并且新的代价小于原代价
					{
						dist[j] = _matrix[u][j] + dist[u];
						pPath[j] = u;
					}
				}
				S[u] = true;//最后将找到的代价最小的节点纳入到集合S中
			}
		}

		bool BellmanFord(const V& src, vector<W>& dist, vector<int>& pPath)//src为传入的源节点,dist为保存从源节点到各个节点下标对应的代价,pPath记录所生成最短路径上每个节点对应的前一个节点
		{
			int n = _vertexs.size();
			int srci = GetVertexIndex(src);
			pPath.resize(n, -1);
			pPath[srci] = srci;
			dist.resize(n, MAX_W);
			dist[srci] = W();

			for (int k = 0; k < n; ++k)//更新n次
			{
				bool update = false;
				//遍历图中的边
				for (int i = 0; i < n; ++i)
				{
					for (int j = 0; j < n; ++j)
					{
						if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])//如果顶点之间相连,且源节点到i顶点再到j顶点的代价小于当前dist中保存的从源节点到j顶点的代价就行更新
						{
							dist[j] = dist[i] + _matrix[i][j];//更新代价
							pPath[j] = i;//更新j在最短路径中前一个节点
							update = true;
						}
					}
				}
				if (update == false)//如果没有发生更新直接跳出
				{
					break;
				}
			}

			bool update = false;
			//再更新一次,如果图带有负权回路,会继续进行更新修正
			for (int i = 0; i < n; ++i)
			{
				for (int j = 0; j < n; ++j)
				{
					if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])//如果顶点之间相连,且源节点到i顶点再到j顶点的代价小于当前dist中保存的从源节点到j顶点的代价就行更新
					{
						dist[j] = dist[i] + _matrix[i][j];//更新代价
						pPath[j] = i;//更新j在最短路径中前一个节点
						update = true;
					}
				}
			}

			return !update;
		}

		void FloydWarShall(vector<vector<W>>& vvDist, vector<vector<int>>& vvParentPath)//vvDist保存从各个节点到各个节点下标对应的代价, vvParentPath记录各个源节点所生成最短路径上每个节点对应的前一个节点
		{
			//初始化vvDist和vvParentPath
			int n = _vertexs.size();
			vvDist.resize(n);
			vvParentPath.resize(n);
			for (int i = 0; i < n; ++i)
			{
				vvDist[i].resize(n, MAX_W);
				vvParentPath[i].resize(n, -1);
				for (int j = 0; j < n; ++j)
				{
					if (_matrix[i][j] != MAX_W)
					{
						vvDist[i][j] = _matrix[i][j];
						vvParentPath[i][j] = i;
					}
					if (i == j)
					{
						vvDist[i][j] = W();
					}
				}
			}

			//更新vvDist和vvParentPath
			for (int k = 0; k < n; ++k)
			{
				for (int i = 0; i < n; ++i)
				{
					for (int j = 0; j < n; ++j)
					{
						//i->j两个节点路径之间经过k节点
						if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W && vvDist[i][k] + vvDist[k][j] < vvDist[i][j])//经过k节点的路径比原路径代价小
						{
							vvDist[i][j] = vvDist[i][k] + vvDist[k][j];//更新i->j的代价

							//更新源节点i到j最短路径的j前一个节点为源节点k到j的前节点(k节点到j节点之间可能会存在多个节点)
							//如果k->j直接相连,上一个点就k, vvpPath[k][j]存就是k
							//如果k->j没有直接相连,k->..->x->j, vpPath[k][j]存就是x
							vvParentPath[i][j] = vvParentPath[k][j];
						}
					}
				}
			}
		}
	private:
		vector<V> _vertexs;//存储顶点
		map<V, int> _indexMap;//存储顶点所对应的邻接矩阵的下标的映射
		vector<vector<W>> _matrix;//邻接矩阵
	};
}


namespace link_table//邻接表存储模式
{
	template<class W>
	struct Edge//存储边的关系
	{
		int _dsti;//目标点的下标
		W _w;//权值
		struct Edge<W>* _next;

		Edge(const int dsti,const W& w)
			:_dsti(dsti),
			_w(w),
			_next(nullptr)
		{}
	};

	template<class V, class W, bool Direction = false>//V为顶点类型,W为边的权值类型,Direction传入的真假值表示是否为有向图
	class Graph
	{
		typedef Edge<W> Edge;

	public:
		Graph(const V* a, size_t n)//传入顶点的集合和顶点数量
		{
			_vertexs.reserve(n);
			for (int i = 0; i < n; ++i)//存储顶点
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;//建立顶点对于邻接矩阵元素下标的映射关系
			}
			_tables.resize(n, nullptr);//初始化邻接表
		}

		int GetVertexIndex(const V& v)//返回顶点对应的矩阵下标
		{
			auto find = _indexMap.find(v);
			if (find != _indexMap.end())
			{
				return find->second;
			}
			else
			{
				cout << v << "顶点不存在" << endl;
				return -1;
			}
		}

		void AddEdge(const V& src, const V& dst, const W& w)
		{
			int s = GetVertexIndex(src);
			int d = GetVertexIndex(dst);
			if (d == s || s == -1 || d == -1)
			{
				cout << "插入的边不合法" << endl;
				return;
			}
			//头插
			Edge* eg = new Edge(d, w);
			eg->_next = _tables[s];
			_tables[s] = eg;
			if (Direction == false)//无向图将两端都链接起来
			{
				Edge* eg = new Edge(s, w);
				eg->_next = _tables[d];
				_tables[d] = eg;
			}
		}

		void Print()//图的打印
		{
			for (int i = 0; i < _tables.size(); ++i)
			{
				cout << _vertexs[i] << "(" << i << "):";
				Edge* cur = _tables[i];
				while (cur)
				{
					cout <<"[" << _vertexs[cur->_dsti] << "(" << cur->_dsti << "),w:" << cur->_w << "] ";
					cur = cur->_next;
				}
				cout << endl;
			}
		}

	private:
		vector<V> _vertexs;//存储顶点
		map<V, int> _indexMap;//存储顶点所对应的邻接表的下标的映射
		vector<Edge*> _tables;//邻接表(出边表)
	};
}

6.2 测试代码

void TestGraph()
{
	matrix::Graph<char, int, INT_MAX, true> g("bacd", 4);
	g.AddEdge('a', 'b', 1);
	g.AddEdge('a', 'd', 4);
	g.AddEdge('b', 'd', 2);
	g.AddEdge('b', 'c', 9);
	g.AddEdge('c', 'd', 8);
	g.AddEdge('c', 'a', 5);
	g.AddEdge('c', 'a', 3);
	g.AddEdge('d', 'c', 6);
	g.Print();
}

void TestGraph2()
{
	string a[] = { "张三", "李四", "王五", "赵六" };
	matrix::Graph<string, int> g(a, 4);
	g.AddEdge("张三", "李四", 100);
	g.AddEdge("张三", "王五", 200);
	g.AddEdge("王五", "赵六", 30);
	g.Print();
}
void TestGraphBFS()
{
	matrix::Graph<char, int> g1("ABCDEFGHI", 9);
	g1.AddEdge('A', 'B', 1);
	g1.AddEdge('A', 'C', 1);
	g1.AddEdge('A', 'D', 1);
	g1.AddEdge('B', 'E', 1);
	g1.AddEdge('B', 'C', 1);
	g1.AddEdge('C', 'F', 1);
	g1.AddEdge('D', 'F', 1);
	g1.AddEdge('E', 'G', 1);
	g1.AddEdge('F', 'H', 1);
	g1.AddEdge('H', 'I', 1);
	g1.BFS('A');
}

void TestGraphDFS()
{
	matrix::Graph<char, int> g1("ABCDEFGHI", 9);
	g1.AddEdge('A', 'B', 1);
	g1.AddEdge('A', 'C', 1);
	g1.AddEdge('A', 'D', 1);
	g1.AddEdge('B', 'E', 1);
	g1.AddEdge('B', 'C', 1);
	g1.AddEdge('C', 'F', 1);
	g1.AddEdge('D', 'F', 1);
	g1.AddEdge('E', 'G', 1);
	g1.AddEdge('F', 'H', 1);
	g1.AddEdge('H', 'I', 1);
	g1.DFS('A');
}

void TestGraphMinTree()
{
	const char str[] = "abcdefghi";
	matrix::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('g', 'i', 6);
	g.AddEdge('h', 'i', 7);
	matrix::Graph<char, int> kminTree;
	cout << "Kruskal:" << g.Kruskal(kminTree) << endl;
	kminTree.Print();
	cout << endl;
	matrix::Graph<char, int> pminTree;
	for (int i = 0; i < sizeof(str) / sizeof(char) - 1; ++i)
	{
		cout << "src: " << str[i] << ", Prim:" << g.Prim(pminTree, str[i]) << endl;
		pminTree.Print();
		cout << endl;
	}
}

void TestGraphDijkstra()
{
	const char* str = "syztx";
	matrix::Graph<char, int, INT_MAX, true> g(str, strlen(str));
	g.AddEdge('s', 't', 10);
	g.AddEdge('s', 'y', 5);
	g.AddEdge('y', 't', 3);
	g.AddEdge('y', 'x', 9);
	g.AddEdge('y', 'z', 2);
	g.AddEdge('z', 's', 7);
	g.AddEdge('z', 'x', 6);
	g.AddEdge('t', 'y', 2);
	g.AddEdge('t', 'x', 1);
	g.AddEdge('x', 'z', 4);
	vector<int> dist;//源节点到下标对应节点最短路径的总权值(代价)
	vector<int> parentPath;//存储最短路径每个节点的前一个节点
	g.Dijkstra('s', dist, parentPath);
	g.PrintShortPath('s', dist, parentPath);
	
	// 图中带有负权路径时,贪心策略则失效了。
	// 测试结果可以看到s->t->y之间的最短路径没更新出来
	/*cout << endl << "下面是带有负权的图" << endl;
	const char* str2 = "sytx";
	matrix::Graph<char, int, INT_MAX, true> g2(str2, strlen(str2));
	g2.AddEdge('s', 't', 10);
	g2.AddEdge('s', 'y', 5);
	g2.AddEdge('t', 'y', -7);
	g2.AddEdge('y', 'x', 3);
	vector<int> dist2;
	vector<int> parentPath2;
	g2.Dijkstra('s', dist2, parentPath2);
	g2.PrintShortPath('s', dist2, parentPath2);*/
}

void TestGraphBellmanFord()
{
	const char* str = "syztx";
	matrix::Graph<char, int, INT_MAX, true> g(str, strlen(str));
	g.AddEdge('s', 't', 6);
	g.AddEdge('s', 'y', 7);
	g.AddEdge('y', 'z', 9);
	g.AddEdge('y', 'x', -3);
	g.AddEdge('z', 's', 2);
	g.AddEdge('z', 'x', 7);
	g.AddEdge('t', 'x', 5);
	g.AddEdge('t', 'y', 8);
	g.AddEdge('t', 'z', -4);
	g.AddEdge('x', 't', -2);
	vector<int> dist;
	vector<int> parentPath;
	if (g.BellmanFord('s', dist, parentPath))
	{
		g.PrintShortPath('s', dist, parentPath);
	}
	else
	{
		cout << "带有负权回路,无法计算" << endl;
	}

	// 图中带有负权路径时,贪心策略则失效了。
	// 测试结果可以看到s->t->y之间的最短路径没更新出来
	/*const char* str = "sytx";
	Graph<char, int, INT_MAX, true> g(str, strlen(str));
	g.AddEdge('s', 't', 10);
	g.AddEdge('s', 'y', 5);
	g.AddEdge('t', 'y', -7);
	g.AddEdge('y', 'x', 3);
	vector<int> dist;
	vector<int> parentPath;
	if (g.BellmanFord('s', dist, parentPath))
	{
		g.PrintShortPath('s', dist, parentPath);
	}
	else
	{
		cout << "带有负权回路,无法计算" << endl;
	}*/
}

void TestFloydWarShall()
{
	const char* str = "12345";
	matrix::Graph<char, int, INT_MAX, true> g(str, strlen(str));
	g.AddEdge('1', '2', 3);
	g.AddEdge('1', '3', 8);
	g.AddEdge('1', '5', -4);
	g.AddEdge('2', '4', 1);
	g.AddEdge('2', '5', 7);
	g.AddEdge('3', '2', 4);
	g.AddEdge('4', '1', 2);
	g.AddEdge('4', '3', -5);
	g.AddEdge('5', '4', 6);
	vector<vector<int>> vvDist;
	vector<vector<int>> vvParentPath;
	g.FloydWarShall(vvDist, vvParentPath);
	// 打印任意两点之间的最短路径
	for (size_t i = 0; i < strlen(str); ++i)
	{
		g.PrintShortPath(str[i], vvDist[i], vvParentPath[i]);
		cout << endl;
	}
}
  • 27
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

1e-12

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

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

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

打赏作者

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

抵扣说明:

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

余额充值