数据结构—图

news2024/11/16 18:57:43

图是在数据结构中难度比较大,并且比较抽象一种数据结构。

图在地图,社交网络这方面有应用。

图的基本概念

图是由顶点集合及顶点间的关系组成的一种数据结构:G=(V,E)。图标的英文:graph。

(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> 是有序的,顶点对 <x y> 称为顶点 x 到顶点 y 的一条
( ) <x, y> <y, x> 是两条不同的边 无向图中,顶点对 (x, y) 是无序的,顶点对 (x,y) 称为顶点 x 和顶点 y 相关联的一条边,这条边没有特定方向, (x, y) (y x) 是同一条边,注意: 无向边 (x, y) 等于有向边 <x, y> <y, x>

树是一种特殊的图(无环联通)。

图不一定是树。

树关注结点中存的值。 

图关注的是顶点及边的权值

图的一些其他的概念:

完全图:在 n 个顶点的无向图中 ,若 n * (n-1)/2 条边 ,即 任意两个顶点之间有且仅有一条边
则称此图为 无向完全图 ,比如上图 G1 ;在 n 个顶点的有向图 中,若 n * (n-1) 条边 ,即 任意两个
顶点之间有且仅有方向相反的边 ,则称此图为 有向完全图。
邻接顶点:在 无向图中 G 中,若 (u, v) E(G) 中的一条边,则称 u v 互为邻接顶点 ,并称 (u,v)
附于顶点 u v ;在 有向图 G 中,若 <u, v> E(G) 中的一条边,则称顶点 u 邻接到 v ,顶点 v 邻接自顶
u ,并称边 <u, v> 与顶点 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 条边

图可以表示城市之间的关系,也还可以表示社交关系。比如顶点是人的话,那么边就是好友,边权值就是亲密度这些。像微信,qq这样的就是无向图,只要是好友,就可以双方互发和接收消息,那么这个也就是强社交关系。像抖音,微博这些,我们关注别人,但是别人没有关注我们,就只能单方面发送消息,而不能接收对方发来的消息,那么这个就是弱社交关系。

图的存储结构

邻接矩阵

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

我们发现无向图的矩阵沿着对角线对称,所以其实可以将矩阵压缩成一半,不过这样的就更加抽象复杂了。

有向图:

如果边带权值,那么我们可以在邻接矩阵中存权值,如果两个结点之间不连通,则可以用特殊数字代替,比如无穷大

 总结:

邻接矩阵有两个优点:

1.它非常适合用来储存稠密图。(稠密图就是相对于稀疏图,它有相对较多的边)

2.邻接矩阵可以用O(1)的时间复杂度来判断两个顶点的关系,并取到权值。

但是它也有一个缺点:

就是它不适合找一个顶点链接的所有边。O(N)复杂度。

邻接表

  邻接表:用数组表示顶点的合集,用链表表示边的关系。

可以看到其实它也有分入边表和出边表,但是大多数情况下就只用出边表。

看着这个结构,我们发现跟思想跟哈希桶很像。

对照邻接矩阵的优缺点来看,邻接表和邻接矩阵属于相辅相成,各有优缺点的互补结构。 

邻接表的简单实现

namespace link_table
{
	template<class W>
	struct Edge
	{
		int _dsti;
		W _w;
		Edge<W>* _next;

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

	template <class V, class W, bool Direction = false>
	class Graph
	{
		typedef Edge<W> Edge;
	public:
		Graph(const V* a, size_t n)
		{
			_vertexs.reserve(n);
			for (size_t i = 0; i < n; ++i)
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;
			}

			_tables.resize(n, nullptr);
		}

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

				return -1;
			}
		}

		void AddEdge(const V& src, const V& dst, const W& w)
		{
			size_t srci = GetVertexIndex(src);
			size_t dsti = GetVertexIndex(dst);

			Edge* eg = new Edge(dsti, w);
			eg->_next = _tables[srci];
			_tables[srci] = eg;

			if (Direction == false)
			{
				Edge* eg = new Edge(srci, w);
				eg->_next = _tables[dsti];
				_tables[dsti] = eg;
			}
		}

		void Print()
		{
			// 顶点
			for (size_t i = 0; i < _vertexs.size(); ++i)
			{
				cout << "[" << i << "]" << "->" << _vertexs[i] << endl;
			}
			cout << endl;

			for (size_t i = 0; i < _tables.size(); ++i)
			{
				cout << _vertexs[i] << "[" << i << "]->";
				Edge* cur = _tables[i];
				while (cur)
				{
					cout << "[" << _vertexs[cur->_dsti] << ":" << cur->_dsti << ":" << cur->_w << "]->";
					cur = cur->_next;
				}
				cout << "nullptr" << endl;
			}
		}

	private:
		vector<V> _vertexs;
		map<V, int> _indexMap;
		vector<Edge*> _tables;
	};

	void TestGraph1()
	{
		/*Graph<char, int, 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();*/

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

图的遍历

图的遍历并不是很难,在实现上有点像树的遍历,也分为广度优先遍历和深度优先遍历。 

以邻接矩阵来实现两种遍历

广度优先遍历 

void BFS(const V& src)
		{
			size_t srci = GetVertexIndex(src);
			// 队列和标记数组
			queue<int> q;
			vector<bool> visited(_vertexs.size(), false);

			q.push(srci);
			visited[srci] = true;
			int levelSize = 1;

			size_t n = _vertexs.size();
			while (!q.empty())
			{
				for (int i = 0; i < levelSize; ++i)
				{
					int front = q.front();
					q.pop();
					cout << front << ":" << _vertexs[front] << " ";
					for (size_t i = 0; i < n; ++i)
					{
						if (_matrix[front][i] != MAX_W)
						{
							if (visited[i] == false)
							{
								q.push(i);
								visited[i] = true;
							}
						}
					}
				}
				cout << endl;

				levelSize = q.size();
			}

			cout << endl;
		}

有点像树的层序遍历 。

深度优先遍历

void _DFS(size_t srci, vector<bool>& visited)
		{
			cout << srci << ":" << _vertexs[srci] << endl;
			visited[srci] = true;

			for (size_t i = 0; i < _vertexs.size(); ++i)
			{
				if (_matrix[srci][i] != MAX_W && visited[i] == false)
				{
					_DFS(i, visited);
				}
			}
		}

		void DFS(const V& src)
		{
			size_t srci = GetVertexIndex(src);
			vector<bool> visited(_vertexs.size(), false);

			_DFS(srci, visited);
		}

加一个bool数组这样的来标记已经访问过的结点。 

最小生成树

若连通图由n个顶点构成,则生成树必包含n个顶点和n-1条边。构成最小生成树有三个准则:

1.只能用图中的边来构造最小生成树。

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

3.选用的n-1条边不能构成回路。

构造最小生成树的算法有kruskal算法和prim算法,二者都是用了贪心策略。

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

kruskal算法

  这个算法就是每次都找到权值最小的边,直到能成为一棵树。这个算法有一个难点就是,我们找边是按最小权值来找的,因此有可能出现的问题。但是之前我们学过并查集,可以把存入的边放入并查集中,这样判断是否会成环就简单多了。

  我们可以将边放入一个小堆当中,这样每次找最小的边时效率非常高。

邻接矩阵+kruskal算法

#pragma once
#include <vector>
#include <map>
#include <string>
#include <queue>
#include <functional>

using namespace std;

namespace matrix
{
	template<class V,class W, W MAX_W = INT_MAX,bool Direction = false>
	class Graph
	{
		typedef Graph<V, W, MAX_W, Direction> Self;
	public:
		Graph() = default;
		Graph(const V* a, size_t n)
		{
			_vertexs.reserve(n);
			for (size_t i = 0; i < n; ++i)
			{
				_vertexs.push_back(a[i]);
				_indexMap[a[i]] = i;
			}

			_matrix.resize(n);
			for (size_t i = 0; i < _matrix.size(); ++i)
			{
				_matrix[i].resize(n, MAX_W);
			}
		}

		size_t GetVertexIndex(const V& v)
		{
			auto it = _indexMap.find(v);
			if (it != _indexMap.end())
			{
				return it->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 << "[" << i << "]" << "->" << _vertexs[i] << endl;
			}
			cout << endl;
			cout << "  ";
			for (size_t i = 0; i < _vertexs.size(); ++i)
			{
				printf("%4d", 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)
					{
						printf("%4c", '#');
					}
					else
					{
						printf("%4d", _matrix[i][j]);
					}
				}
				cout << endl;
			}
			cout << endl;
		}

		void BFS(const V& src)
		{
			size_t srci = GetVertexIndex(src);
			// 队列和标记数组
			queue<int> q;
			vector<bool> visited(_vertexs.size(), false);

			q.push(srci);
			visited[srci] = true;
			int levelSize = 1;

			size_t n = _vertexs.size();
			while (!q.empty())
			{
				for (int i = 0; i < levelSize; ++i)
				{
					int front = q.front();
					q.pop();
					cout << front << ":" << _vertexs[front] << " ";
					for (size_t i = 0; i < n; ++i)
					{
						if (_matrix[front][i] != MAX_W)
						{
							if (visited[i] == false)
							{
								q.push(i);
								visited[i] = true;
							}
						}
					}
				}
				cout << endl;

				levelSize = q.size();
			}

			cout << endl;
		}

		void _DFS(size_t srci, vector<bool>& visited)
		{
			cout << srci << ":" << _vertexs[srci] << endl;
			visited[srci] = true;

			for (size_t i = 0; i < _vertexs.size(); ++i)
			{
				if (_matrix[srci][i] != MAX_W && visited[i] == false)
				{
					_DFS(i, visited);
				}
			}
		}

		void DFS(const V& src)
		{
			size_t srci = GetVertexIndex(src);
			vector<bool> visited(_vertexs.size(), false);

			_DFS(srci, visited);
		}

		struct Edge
		{
			size_t _srci;
			size_t _dsti;
			W _w;

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

			bool operator>(const Edge& e)const
			{
				return _w > e._w;
			}
		};

		W Kruskal(Self& minTree)
		{
			size_t n = _vertexs.size();

			minTree._vertexs = _vertexs;
			minTree._indexMap = _indexMap;
			minTree._matrix.resize(n);
			for (size_t i = 0; i < n; ++i)
			{
				minTree._matrix[i].resize(n, MAX_W);
			}

			priority_queue<Edge, vector<Edge>, greater<Edge>> minque;
			for (size_t i = 0; i < n; ++i)
			{
				for (size_t j = 0; j < n; ++j)
				{
					if (i < j && _matrix[i][j] != MAX_W)
					{
						minque.push(Edge(i, j, _matrix[i][j]));
					}
				}
			}

			// 选出n-1条边
			int size = 0;
			W totalW = W();
			UnionFindSet ufs(n);
			while (!minque.empty())
			{
				Edge min = minque.top();
				minque.pop();

				if (!ufs.InSet(min._srci,min._dsti))  // 判断是否构成环
				{
					cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
					minTree.AddEdge(min._srci, min._dsti, min._w);
					ufs.Union(min._srci, min._dsti);
					++size;
					totalW += min._w;
				}
				else
				{
					cout << "构成环: ";
					cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
				}
			}

			if (size == n - 1)
			{
				return totalW;
			}
			else
			{
				return W(); // 说明不能生成一棵树
			}
		}
	private:
		vector<V> _vertexs;   // 顶点集合
		map<V, int> _indexMap;// 顶点映射下标
		vector<vector<W>> _matrix; // 邻接矩阵
	};

	void TestBDFS()
	{
		string a[] = { "张三", "李四", "王五", "赵六", "周七" };
		Graph<string, int> g1(a, sizeof(a) / sizeof(string));
		g1.AddEdge("张三", "李四", 100);
		g1.AddEdge("张三", "王五", 200);
		g1.AddEdge("王五", "赵六", 30);
		g1.AddEdge("王五", "周七", 30);
		g1.Print();

		g1.BFS("张三");
		g1.DFS("张三");
	}

	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('a', 'h', 9);
		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);

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

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

		for (size_t i = 0; i < strlen(str); ++i)   // 测试不同从不同源点使用Prim算法结果是否相同
		{
			cout << "Prim:" << g.Prim(pminTree, str[i]) << endl;
		}
	}
}

这个邻接矩阵的设计就是先把顶点都传入,先构造顶点和编号的映射关系,然后再手动的添加顶点之间边的关系及这个边的权值。 

Prim算法 

  kruskal算法是每次直接找权值最小的边,直到能生成一个树,这样的问题就是可能会生成环。

  而Prim算法就是专门针对了会生成环这个问题,因为一样也需要将边放到堆中以便每次能选出权值最小的边,Prim算法将顶点分成了两个集合,一个集合里放着已经是树的顶点了,另一个集合放着尚未成为树的顶点,每次从已经生成树集合里面的顶点选边,如果这个边是与已经是树的顶点连接的,那么肯定会成环,所以就不选。这样一来就可以达到避免生成环的效果,选完之后再将这个顶点转移到放着已经是树的顶点集合中,直到堆中的数据为空。所以本策略也是贪心策略。

示意图:

代码实现依旧是使用邻接矩阵,并且是对上一个代码的补充,在同一个类中。

并且,用来表示顶点的集合其实用一个bool数组就可以都标识了。并且与kruskal算法不同的是,我们可以传入一个源点,可以从任意一个点开始生成树。

W Prim(Self& minTree, const V& src)
		{
			size_t srci = GetVertexIndex(src);
			size_t n = _vertexs.size();

			minTree._vertexs = _vertexs;
			minTree._indexMap = _indexMap;
			minTree._matrix.resize(n);
			for (size_t i = 0; i < n; ++i)
			{
				minTree._matrix[i].resize(n, MAX_W);
			}

			vector<bool> X(n, false);  // 其实用一个数组即可完成
			X[srci] = true;

			// 先把srci的边放入堆中,kruskal是直接全放入堆中
			priority_queue<Edge, vector<Edge>, greater<Edge>> minq;
			for (size_t i = 0; i < n; ++i)
			{
				if (_matrix[srci][i] != MAX_W)
				{
					minq.push(Edge(srci, i, _matrix[srci][i]));
				}
			}

			cout << "Prim Start!" << endl;
			size_t size = 0;
			W totalW = W();
			while (!minq.empty())
			{
				Edge min = minq.top();
				minq.pop();

				if (X[min._dsti])  // 如果最小边的目标点也在X集合,则会构成环
				{
					cout << "构成环:";
					cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << ":" << min._w << endl;
				}
				else
				{
					minTree._AddEdge(min._srci, min._dsti, min._w);
					X[min._dsti] = true;  // 记得更新X集合
					++size;
					totalW += min._w;
					if (size == n - 1)
						break;

					//记得更新下一次需要用到的边
					for (size_t i = 0; i < n; ++i)
					{
						if (_matrix[min._dsti][i] != MAX_W)
						{
							minq.push(Edge(min._dsti, i, _matrix[min._dsti][i]));
						}
					}
				}
			}

			if (size == n - 1)
			{
				return totalW;
			}
			else
			{
				return W();
			}
		}

两个算法的优缺点 

Kruskal算法的优点:

  1. 适用于稀疏图:Kruskal算法在边的数量相对较少的情况下效率较高,因此在稀疏图中表现较好。
  2. 简单易实现:Kruskal算法的实现相对简单,只需要对边进行排序,然后依次选择权值最小的边加入最小生成树即可。

Kruskal算法的缺点:

  1. 需要排序:Kruskal算法需要对所有的边进行排序,因此在边的数量较多时,排序的时间复杂度较高。
  2. 需要并查集:Kruskal算法需要使用并查集来判断选择的边是否形成环路,这增加了算法的复杂度。

Prim算法的优点:

  1. 适用于稠密图:Prim算法在顶点的数量相对较多而边的数量相对较少的情况下效率较高,因此在稠密图中表现较好。
  2. 使用邻接表方便:Prim算法使用邻接表来表示图时,可以方便地进行顶点的选择和距离的更新。

Prim算法的缺点:

  1. 需要选择顶点:Prim算法每次需要选择距离当前生成树最近的顶点,这需要遍历所有的顶点来找到最近的顶点,增加了算法的时间复杂度。
  2. 可能产生不唯一的解:在某些情况下,Prim算法可能会产生不唯一的最小生成树。

综上所述,Kruskal算法和Prim算法各有优劣,选择哪种算法取决于具体的应用场景和图的特性。在稀疏图中,Kruskal算法可能更为适合;而在稠密图中,Prim算法可能更有优势。

最短路径

 图最难,也是最后一个学习的部分就是最短路径问题了,就是找到最短路径。有Dijkstra算法和Bellman-ford算法,是解决单源最短路径的,Floyd-Warshall是解决多源最短路径问题的。

Dijkstra算法

   Dijkstra算法是一种用于解决单源最短路径问题的贪心算法,由荷兰计算机科学家狄克斯特拉于1959年提出。该算法采用广度优先搜索的思想,从起始点开始,逐步扩展到其他顶点,直到找到从起始点到所有其他顶点的最短路径。

 它的思路跟Prim算法有点想,也是用两个集合A,B对顶点进行标记。首先是初始化,将所有顶点的值比如可以初始化成无穷大,源点的值可以初始化成0,毕竟自己走到自己的花费一般都是0。然后根据边的权值更新从这个顶点出发到目标点的权值(这个顶点要满足不在A集合在B集合的条件),然后取最小的一个点作为下一次的起点,并放入A集合中,以此不断更新。这也是贪心策略

 另外记得要把路径记录下来,也可以使用数组来记录。

代码,同样要给一个源点外,我们可以通过输出型参数的方式把结果带出去。

// 因为我们存的pPath是从子找源点,打印的时候我们需要将其倒过来
		void PrintShortPath(const V& src, const vector<W>& dist, const vector<int>& pPath)
		{
			size_t srci = GetVertexIndex(src);
			size_t n = _vertexs.size();
			for (size_t i = 0; i < n; ++i)
			{
				if (i != srci)
				{
					// 找出i顶点的路径
					vector<int> path;
					size_t parenti = i;
					while (parenti != srci)
					{
						path.push_back(parenti);
						parenti = pPath[parenti];
					}
					path.push_back(srci);
					reverse(path.begin(), path.end());  // 逆置过来

					for (auto index : path)
					{
						cout << _vertexs[index] << "->";
					}
					cout << "# 权值和为:" << dist[i] << endl;
				}
			}
		}

		void Dijkstra(const V& src,vector<W>& dist,vector<int>& pPath)
		{
			size_t srci = GetVertexIndex(src);
			size_t n = _vertexs.size();
			dist.resize(n, MAX_W);
			pPath.resize(n, -1); // 用这个来储存路径,其原理很像并查集,每个编号存自己的父节点以此来找到源点

			dist[srci] = 0;  // 源点要初始成0
			pPath[srci] = srci;  // 路径集合也要特殊处理

			//已经确定最短路径的顶点集合
			vector<bool> S(n, false);

			for (size_t j = 0; j < n; ++j)
			{
				// 选最短路径顶点,且不在S。 还要更新其他路径
				int u = 0;  // 当前路径最短的顶点
				W min = MAX_W;
				for (size_t i = 0; i < n; ++i)
				{
					if (S[i] == false && dist[i] < min)
					{
						u = i;
						min = dist[i];
					}
				}

				S[u] = true;
				
				//如果不在最短路径的顶点的权值出现了更低的值,需要更新成最低的,同时也要更新这个u->v的路径。
				for (size_t v = 0; v < n; ++v)
				{
					if (S[v] == false && _matrix[u][v] != MAX_W
						&& dist[u] + _matrix[u][v] < dist[v])
					{
						dist[v] = dist[u] + _matrix[u][v];
						pPath[v] = u;
					}
				}
			}
		}


// 补充测试函数,放在类外,命名空间内
void TestGraphDijkstra()
	{
		const char* str = "syztx";
		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之间的最短路径没更新出来
		/*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;
		g.Dijkstra('s', dist, parentPath);
		g.PrintShortPath('s', dist, parentPath);*/
	}
}

 运行结果可以对照:

但是Dijkstra算法不能支持带负权路径的图,这样会使算法失效,算出错误的结果。

  因为我们自习想想这个贪心策略,它是在我们选到这个结点后,不可能再会有到这个结点更小的路径的结果的基础上进行的,如果存在负权,那么这个条件就不一定满足。它每次是以最短路径去更新的。

  Dijkstra算法的时间复杂度为O(N^2),空间复杂度为O(N)。

Bellman-Ford算法

  Dijkstra算法的优点就是效率高,但是存在如果有负权就失效的缺点。Bellman-Ford算法解决了不能计算负权的缺点,但是这是在牺牲效率的前提下。Bellman-Ford算法它采取的是一种暴力的搜索方式, 因此时间复杂度是O(N^3),空间复杂度也是O(N)。

代码

bool BellmanFord(const V& src,vector<W>&dist, vector<int>&pPath)
		{
			size_t n = _vertexs.size();
			size_t srci = GetVertexIndex(src);

			dist.resize(n, MAX_W);
			pPath.resize(n, -1);
			dist[srci] = W();

			for (size_t k = 0; k < n; ++k)
			{
				bool update = false;   // 一个小优化,如果次轮已经没有更新,那么就可以直接退出了
				cout << "更新第:" << k << "轮" << endl;
				for (size_t i = 0; i < n; ++i)
				{
					for (size_t j = 0; j < n; ++j)
					{
						if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
						{
							update = true;
							cout << _vertexs[i] << "->" << _vertexs[j] << ":" << _matrix[i][j] << endl;
							dist[j] = dist[i] + _matrix[i][j];
							pPath[j] = i;
						}
					}
				}

				if (update == false)
				{
					break;
				}
			}

			// 如果还能更新说明存在负权回路
			for (size_t i = 0; i < n; ++i)
			{
				for (size_t j = 0; j < n; ++j)
				{
					if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
					{
						return false;
					}
				}
			}

			return true;
		}

//补充同理

void TestGraphBellmanFord()
	{
			const char* str = "syztx";
			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);

		//const char* str = "syztx";
		//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('y', 's', 1); // 新增
		//g.AddEdge('z', 's', 2);
		//g.AddEdge('z', 'x', 7);
		//g.AddEdge('t', 'x', 5);
		g.AddEdge('t', 'y', -8); //更改
		//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;
	}

测试结果参考此图

 

  看代码就知道,这个算法的时间其实很简单,直接三层循环。为什么要更新n次呢?比如更新第一次的时候,每更新一个新的更小路径,就有可能影响到之前已经更新过的路径,因此需要再更新一次,同理这次更新还有可能又会影响到其他的路径,最坏的情况下要更新n次。

在这个代码中我们还进行了一个小优化,那就是如果此次循环并没更新新的路径,那么就退出。

  还有一个优化思想就是,除了第一次更新,往后所有的更新我们只需要更新那些被后面更新影响到的路径即可,不需要所有路径都再更新一次。

  另外我们发现Bellman-Ford是有返回值的,这是为了判断这个图中是否有存在负权回路。负权回路就是从源点出发,更新一圈后发现源点到源点的值居然变小了也就是变成负数了,并且每一次更新都会变得更小。

为什么Dijkstra算法不担心呢?因为它连负权都解决不了。 

并且负权回路问题不是算法能解决的,是问题本身出了问题。

Floyd-Warshall算法

  这个算法不同于之前两个算法,之前两个算法是解决单源最短路径的,而Floyd-Warshall算法是解决多源最短路径问题的。也就是能求出任意两个点之间的最短路径。这个算法也是这三个算法中最抽象的,并且Floyd-Warshall用的动态规划的思想。

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

 

 代码:

void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath)
		{
			size_t n = _vertexs.size();
			vvDist.resize(n);
			vvpPath.resize(n);

			// 初始化权值和路径矩阵
			for (size_t i = 0; i < n; ++i)
			{
				vvDist[i].resize(n, MAX_W);
				vvpPath[i].resize(n, -1);
			}

			// 直接相连的边更新一下
			for (size_t i = 0; i < n; ++i)
			{
				for (size_t j = 0; j < n; ++j)
				{
					if (_matrix[i][j] != MAX_W)
					{
						vvDist[i][j] = _matrix[i][j];
						vvpPath[i][j] = i;
					}

					if (i == j)
					{
						vvDist[i][j] = W();
					}
				}
			}

			// 为什么不是n - 2呢,因为 a -> b, 和c -> d,虽然需要遍历的最大次数同时n - 2,但是结果却不同
			for (size_t k = 0; k < n; ++k)
			{
				for (size_t i = 0; i < n; ++i)
				{
					for (size_t j = 0; j < n; ++j)
					{
						// 这里的k是作为中间结点去更新i->j 的路径。
						if (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W
							&& vvDist[i][k] + vvDist[k][j] < vvDist[i][j])  // 注意这里的条件是理解关键
						{
							vvDist[i][j] = vvDist[i][k] + vvDist[k][j];

							// 注意下面关于路径的更新
							// 是找j相连的上一个邻接顶点
							// 如果k->j 直接相连,那么j的上一个就是k,所以vvpPath[k][j]存的就是k
							// 如果k->j 没有直接相连,比如 k->...->x->j,那么vvpPath[k][j]存的就是x

							vvpPath[i][j] = vvpPath[k][j];
						}
					}
				}
			}

			// 打印权值和路径矩阵来观察数据
			for (size_t i = 0; i < n; ++i)
			{
				for (size_t j = 0; j < n; ++j)
				{
					if (vvDist[i][j] == MAX_W)
					{
						printf("%3c", '#');
					}
					else
					{
						printf("%3d", vvDist[i][j]);
					}
				}
				cout << endl;
			}
			cout << endl;

			// 再打印路径图
			for (size_t i = 0; i < n; ++i)
			{
				for (size_t j = 0; j < n; ++j)
				{
					printf("%3d", vvpPath[i][j]);
				}
				cout << endl;
			}
			cout << "======================================" << endl;
		}

// 补充同理
void TestFloydWarShall()
	{
		const char* str = "12345";
		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;
		}
	}

 结果参考图

 Floyd-Warshall算法的时间复杂度为O(N^3),空间复杂度为O(N^2)。

其实找任意两个结点的最短路径Dijkstra和Bellman-Ford算法也可以做到,无非就是再套一层循环,Dijkstra再套一层循环时间复杂度是O(N^3),效率跟Floyd-Warshall是一样的,但是无法解决负权问题,Bellman-Ford再套一层就O(N^4),效率就太低了。

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

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

相关文章

Rust Vs Go:从头构建一个web服务

Go 和 Rust 之间的许多比较都强调它们在语法和初始学习曲线上的差异。然而&#xff0c;最终的决定性因素是重要项目的易用性。 “Rust 与 Go”争论 Rust vs Go 是一个不断出现的话题&#xff0c;并且已经有很多关于它的文章。部分原因是开发人员正在寻找信息来帮助他们决定下…

数据分析在企业培训系统中的关键作用与优势

数据分析在企业培训系统中扮演着关键的角色&#xff0c;它不仅能够帮助企业更好地了解员工培训的需求和效果&#xff0c;还能够提供有针对性的教育方案和提高培训效果。 数据分析可以帮助企业准确把握员工培训需求。通过收集和分析员工的培训需求调查和反馈信息&#xff0c;企…

浅谈WPF之利用RichTextBox实现富文本编辑器

在实际应用中&#xff0c;富文本随处可见&#xff0c;如留言板&#xff0c;聊天软件&#xff0c;文档编辑&#xff0c;特定格式内容等&#xff0c;在WPF开发中&#xff0c;如何实现富文本编辑呢&#xff1f;本文以一个简单的小例子&#xff0c;简述如何通过RichTextBox实现富文…

JavaCV之rtmp推流(FLV和M3U8)

JavaCV与FFmpeg FFmpeg是一款开源的多媒体处理工具集&#xff0c;它包含了一系列用于处理音频、视频、字幕等多媒体数据的库和工具。 JavaCV集成了FFmpeg库&#xff0c;使得Java开发者可以使用FFmpeg的功能&#xff0c;比如视频解码、编码、格式转换等。 除了FFmpeg&#xff…

01_02_mysql07_mysql8.0新特性

1.MySQL8新特性概述 MySQL从5.7版本直接跳跃发布了8.0版本 &#xff0c;可见这是一个令人兴奋的里程碑版本。MySQL 8版本在功能上做了显著的改进与增强&#xff0c;开发者对MySQL的源代码进行了重构&#xff0c;最突出的一点是多MySQL Optimizer优化器进行了改进。不仅在速度上…

在VsCode中通过Cookie登录LeetCode

在vscode中配置好leetcode之后&#xff0c;一般最常用的就是通过cookie登录leetcode ; 首先点击sign in &#xff0c; 然后选择最下面的 &#xff0c; LeetCode Cookie ! 然后输入username(也就是你的lc用户名) 或者 你leetcode绑定的邮箱 ; 输入完成之后 ; 就是要你输入你的l…

【Java EE初阶二十二】https的简单理解

1. 初识https 当前网络上,主要都是 HTTPS 了,很少能见到 HTTP.实际上 HTTPS 也是基于 HTTP.只不过 HTTPS 在 HTTP 的基础之上, 引入了"加密"机制&#xff1b;引入 HTTPS 防止你的数据被黑客篡改 &#xff1b; HTTPS 就是一个重要的保护措施.之所以能够安全, 最关键的…

C#知识点-14(索引器、foreach的循环原理、泛型、委托)

索引器 概念&#xff1a;索引器能够让我们的对象&#xff0c;以索引&#xff08;下标&#xff09;的形式&#xff0c;便捷地访问类中的集合&#xff08;数组、泛型集合、键值对&#xff09; 应用场景&#xff1a; 1、能够便捷地访问类中的集合 2、索引的数据类型、个数、顺序不…

从源码解析Kruise(K8S)原地升级原理

从源码解析Kruise原地升级原理 本文从源码的角度分析 Kruise 原地升级相关功能的实现。 本篇Kruise版本为v1.5.2。 Kruise项目地址: https://github.com/openkruise/kruise 更多云原生、K8S相关文章请点击【专栏】查看&#xff01; 原地升级的概念 当我们使用deployment等Wor…

vue:find查找函数实际开发的使用

find的作用&#xff1a; find 方法主要是查找数组中的属性&#xff0c;会遍历数组&#xff0c;对每一个元素执行提供的函数&#xff0c;直到找到使该函数返回 true 的元素。然后返回该元素的值。如果没有元素满足测试函数&#xff0c;则返回 undefined。 基础使用&#xff1a…

摄像头相机标定

相机标定 相机标定的目的有两个。 第一&#xff0c;要还原摄像头成像的物体在真实世界的位置就需要知道世界中的物体到计算机图像平面是如何变换的&#xff0c;相机标定的目的之一就是为了搞清楚这种变换关系&#xff0c;求解内外参数矩阵。 第二&#xff0c;摄像机的透视投影有…

linux下开发,stm32和arduino,我该何去何从?

linux下开发&#xff0c;stm32和arduino&#xff0c;我该何去何从&#xff1f; 在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「stm3的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“888”之后私信回复“888”&#xff0c;全部无偿共…

zemax消畸变目镜

用三胶合透镜代替了RKE的消色差双胶合镜&#xff0c;减少了横向色差和畸变 入瞳直径4mm波长0.51、0.56、0.61半视场22.5焦距28mm 镜头参数&#xff1a; 成像效果&#xff1a; 畸变效果&#xff1a; 点列图&#xff1a;

S281 LoRa网关助力智慧城市建设的智能交通管理

S281 LoRa网关作为智慧城市建设中的重要组成部分&#xff0c;发挥着关键的作用&#xff0c;特别是在智能交通管理方面。通过连接各类传感器设备和物联网终端&#xff0c;S281 LoRa网关实现了对城市交通系统的远程监控、智能调度和信息化管理&#xff0c;为城市交通管理部门提供…

UE5 摄像机晃动

1.新建camerashake蓝图类 命名为 晃动 2.调节相关参数 3.打开关卡序列 给摄像机添加 晃动 动画 4.播放

#gStore-weekly | workbench功能详解之知识更新

gStore workbench作为gStore的可视化管理工具&#xff0c;不仅提供了可视化查询功能&#xff0c;还提供了可视化的知识更新功能&#xff0c;用户可以在可视化界面上进行知识的新增、修改和删除等操作&#xff0c;让我们的知识管理更加清晰和便捷。 1.查询知识 登录workbenc…

新年伊始,VR全景释放“强信号”,可以结合哪些行业?

一年之计在于春&#xff0c;各行各业都想抢占在经济的第一线&#xff0c;那么如何抓住新一轮科技革命和产业变革新机遇呢&#xff1f;VR全景释放了“强信号”。对于大部分实体行业来说&#xff0c;都会有VR全景的制作需求&#xff0c;租房买房的&#xff0c;可能都见识过线上VR…

Manacher算法和扩展kmp

Manacher算法 a情况 b情况 具体例子 c情况 总结 代码 #include<iostream> #include<algorithm> #include<string> #include<cmath>using namespace std; const int N 1.1e7 1; char ss[N << 1]; int p[N << 1]; int n; void manacherss…

Qt应用-视频播放器实例

本文讲解Qt视频播放器应用实例。 实现功能 视频的播放暂停、拖动进度控制,声音控制播放列表控制播放区域的暂停控制,全屏控制等。 界面设计 <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"><class>frmVide…

c#高级-正则表达式

正则表达式是由普通字符和元字符&#xff08;特殊符号&#xff09;组成的文字形式 应用场景 1.用于验证输入的邮箱是否合法。 2.用于验证输入的电话号码是否合法。 3.用于验证输入的身份证号码是否合法。等等 正则表达式常用的限定符总结&#xff1a; 几种常用的正则简写表达式…