【高阶数据结构】图论

> 作者:დ旧言~

> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:了解什么是图,并能掌握深度优先遍历和广度优先遍历。

> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!

> 专栏选自:数据结构

> 望小伙伴们点赞👍收藏✨加关注哟💕💕

一、图的基本概念


1.1 图的定义

概念:

图是一种非线性的数据结构:G=(V,E),它由节点(也称为顶点)和连接这些节点的边组成。图可以用来表示现实世界中的各种关系,如社交网络、交通网络、电路网络等。

1.2 术语解释

  • 顶点(Vertex):图中的基本元素,可以表示任何实体,如人、地点、事物等。顶点集合V={x|x属于某个数据对象集}是有穷非空集合
  • 边(Edge):连接两个顶点的线,表示顶点之间的关系。边的集合:E={(x,y)|x,y属于V}(无向图)或者E={<x,y>|x,y属于V&&Path(x,y)}是顶点间关系的有穷集合
  • 邻接(Adjacent):如果两个顶点通过一条边直接相连,则称这两个顶点是邻接的。
  • 度(Degree):对于无向图而言,一个顶点的度是指与该顶点相连的边的数量。但是对于有向图来说,一个顶点的度分为入度和出度,顶点的入度是以该顶点为终点的有向边的条数,出度则是以该顶点为起点的有向边的条数,顶点的度等于入度和出度之和
  • 路径(Path):顶点序列,其中每对连续的顶点都是邻接的。
  • 环(Cycle):路径中的第一个和最后一个顶点是同一个顶点的情况。
  • 连通图:对于任意两个顶点,都存在一条路径连接它们,即图中每一个顶点都不是单独存在的,它们都可以通过各种路径互相到达(如果有顶点单独存在,没有与其它任意顶点相连,则称这个顶点为一个孤岛)
  • 连通分量:一个图中的不连通部分。

1.3 图的分类

无向图(Undirected Graph)

连接两个顶点的边没有方向,没有方向意味着两个顶点是互相连通的,这种常见的如朋友关系图:我是你的好友,同样你也是我的好友

有向图(Directed Graph)

边有方向,有方向意味着一个顶点可以到另一个顶点,但是反过来不行,这种关系常见的如网页链接图:我可以点击这个链接跳到一个图片,但是无法通过这个图片再跳回原来的界面。

简单图(Simple Graph)

没有重复的边和自环(顶点连接到自身的边)。

多重图(Multigraph)

允许有重复的边和自环。

带权图(Weighted Graph)

每条边上都有权重,一般是数字,可以表示距离、成本等。比如,我们在修从杭州到西安的高铁时,我们可以选择经过郑州,也可以选择经过武汉,这就产生了不同的路径,我们可以比较两个路径的开支来选择经过哪个城市,这就是权值

二、图的存储结构

因为图中既有节点,又有边(节点与节点之间的关系),因此,在图的存储中,只需要保存:节点和

边关系即可。节点保存比较简单,只需要一段连续空间即可,那边关系该怎么保存呢?

2.1 邻接矩阵

邻接矩阵概念

使用二维数组来表示图,其中矩阵的元素表示顶点之间的连接情况,顶点之间的关系只有连通与不连通,所以我们可以用0和1来表示。

注意:

  • 无向图的邻接矩阵是对称的,但是有向图的邻接矩阵不一定对称。
  • 如果边上带权值,可以用权值来代替上面的0和1,相连通的顶点可以用权值来表示,不连通的可以用无穷来表示。
  • 邻接矩阵的有点是可以直观的看出两个顶点之间是否相连,但是当顶点过多、边过少的时候,就会存储大量的0,就会很不方便。

简单模拟实现:

cpp 复制代码
#include<iostream>
#include<vector>
#include<string>
#include<map>
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();
}
int main()
{
	TestGraph();
	return 0;
}

2.2 邻接表

邻接表概念

使用列表来表示图,每个顶点对应一个列表,列表中包含所有与该顶点相连的顶点。

无向邻接表存储:

有向邻接表存储:

注意:

有向图中每条边在邻接表中只出现一次,与顶点vi对应的邻接表所含结点的个数,就是该顶点的出度,也称出度表,要得到vi顶点的入度,必须检测其他所有顶点对应的边链表,看有多少边顶点的dst取值是i。

简单模拟实现:

cpp 复制代码
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);
}

2.3 边列表(了解)

边列表概念:

使用列表来存储图中的所有边,每条边由两个顶点表示。(这个不常用,在这里不做过多解释,想要了解的可以自行搜索一下)

2.4 邻接矩阵和邻接表的优劣性

邻接矩阵优劣性:

  • 邻接矩阵的存储方式非常适合稠密图。
  • 邻接矩阵O(1)判断两个顶点的连接关系,并取到权值。
  • 不适合查找一个顶点连接的所有边------O(N)。

邻接表优劣性:

  • 邻接表的存储方式非常适合稀疏图。
  • 适合找一个顶点连出去的所有边。
  • 不适合确定两点是否相连以及权值------O(N)。

三、图的遍历


3.1 广度优先遍历

图解:

简单模拟实现:

cpp 复制代码
层序遍历 但是没有一层一层出
void BFS(const V& src) //src表示我们的起点
{
	size_t srci = GetVertexIndex(src);//找到起点的下标
	queue<int> q;//存储下标的队列
	size_t n = _vertexs.size();//表示有多少个节点
	vector<bool> check(n);
	q.push(srci);
	check[srci] = true;//入队列就标记
	while (!q.empty())
	{
		int front = q.front();//取队头
		q.pop();
		cout << _vertexs[front] << ":"<<front<<endl;
		//然后让他的朋友进
		for (size_t i = 0; i < n; ++i)
			if (_matrix[front][i] != MAX_W && check[i] == false)
			{
				q.push(i);
				check[i] = true;
			}
	}
}

如果你对广度优先遍历算法感兴趣的话,大家可以康康下面的算法题:

刷题训练之解决 FloodFill 算法-CSDN博客

3.2 深度优先遍历

图解:

简单模拟实现:

cpp 复制代码
void _DFS(size_t srci, vector<bool>& check) //下一个位置的起点以及需要一个标记数组
{
	cout << srci << ":" << _vertexs[srci] << " " << endl; //先访问 然后标记为访问过
	check[srci] = true;
	for (size_t i = 0; i < _vertexs.size(); ++i)
		if (_matrix[srci][i] != MAX_W && check[i] == false)
			_DFS(i, check);
}
 
 
void DFS(const V& src) //需要有一个起点
{
	size_t srci = GetVertexIndex(src);//找到起点的下标
	vector<bool> check(_vertexs.size()); //标记数组
	_DFS(srci, check);
	//如果是一个非连通图,可以在后面再进行一层检查check数组,然后对false的数组再进行一次访问
}

如果你对深度优先遍历算法感兴趣的话,大家可以康康下面的算法题:

刷题训练之解决最短路径问题-CSDN博客

四、最小生成树

在了解最小生成树算法之前,我们首先要先了解以下的准则:

连通图中的每一棵生成树,都是原图的一个极大无环子图,即:从其中删去任何一条边,生成树就不在连通;反之,在其中引入任何一条新边,都会形成一条回路。

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

  • 只能使用图中的边来构造最小生成树。
  • 只能使用恰好n-1条边来连接图中的n个顶点。
  • 选用的n-1条边不能构成回路(少一条就不连通,多一条就会形成回路)。

4.1 Kruskal算法

算法说明:

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

核心:

每次迭代时,选出一条具有最小权值,且两端点不在同一连通分量上的边(意思就是不能导致成环),加入生成树。

简单模拟实现:

cpp 复制代码
namespace Matrix  //以邻接矩阵的形式封装
{
	template<class V,class W, W MAX_W = INT_MAX,bool Direction = false>   //V表示顶点  W表示权重  MAX_W表示默认的权重值  Direction表示是有向图还是无向图    后面两个是非类型模版参数(缺省) 
	class Graph
	{
	public:
		//图创建的方式
		//1、io输入  不方便测试 再oj中较为合适
		//2、图结构关系写到文件,读取文件
		//3、手动去添加边,这样会更方便测试!!
		Graph(const V* vertexs,size_t n) //传一个顶点相关的集合进行初始化
		{
			_vertexs.reserve(n);
			for (size_t i = 0; i < n; ++i) //初始化顶点集合
			{
				_vertexs.push_back(vertexs[i]);//存到顶点集合里
				_IndexMap[vertexs[i]] = i;//建立顶点和下标的映射关系,方便我们进行查找
			}
			//初始化邻接矩阵
			_matrix.resize(n);
			for (auto& e : _matrix)   e.resize(n, MAX_W);
		}
 
		//获取顶点的下标(为什么要单独给这样一个函数呢?因为可能给的是一个错误的顶点,要检查一下)
		size_t GetVertexIndex(const V& v)
		{
			//有可能顶点会给错,这样在map中就找不到 所以要先检查一下
			auto it = _IndexMap.find(v);
			if (it != _IndexMap.end())  return it->second;
			else
			{
				throw invalid_argument("不存在的顶点");//抛异常(异常被捕获后可以不做处理)
				//断言太暴力了,会直接终止程序,并且断言在release版本下会被屏蔽
 
				//异常被捕获后可以不作处理,程序从捕获位置继续执行。 而断言是完全无法忽略的,程序在断言失败处立即终止。
				//  因此断言通常用于调试版本,用来发现程序中的逻辑错误。 虽然异常也能起到这样的作用,但是不应该用异常代替断言: 
				// 1) 如果发现了逻辑错误,必须修改程序,而不可能在程序中进行处理和恢复,所以不需要向外传送,没有必要使用异常。
				//  2) 使用断言的开销比异常小得多,而且断言可以从发布版中完全去除。
 
				return -1; //还是要返回,因为编译器不会在乎运行,而是会检查你在这边有没有返回
			}
		}
 
	    //利用序号为两个节点添加边
		void _AddEdge(size_t srci, size_t dsti, const W& w)
		{
			_matrix[srci][dsti] = w;
			//如果是无向图
			if (Direction == false)   _matrix[dsti][srci] = w;
		}
 
 
		//对两个节点添加边,以及权重关系  src代表起点 dst代表中点 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 BFS(const V& src) //src表示我们的起点
		//{
		//	size_t srci = GetVertexIndex(src);//找到起点的下标
		//	queue<int> q;//存储下标的队列
		//	size_t n = _vertexs.size();//表示有多少个节点
		//	vector<bool> check(n);
		//	q.push(srci);
		//	check[srci] = true;//入队列就标记
		//	while (!q.empty())
		//	{
		//		int front = q.front();//取队头
		//		q.pop();
		//		cout << _vertexs[front] << ":"<<front<<endl;
		//		//然后让他的朋友进
		//		for (size_t i = 0; i < n; ++i)
		//			if (_matrix[front][i] != MAX_W && check[i] == false)
		//			{
		//				q.push(i);
		//				check[i] = true;
		//			}
		//	}
		//}
 
		//控制一层一层出
		void BFS(const V& src) //src表示我们的起点
		{
			size_t srci = GetVertexIndex(src);//找到起点的下标
			queue<size_t> q;//存储下标的队列
			size_t n = _vertexs.size();//表示有多少个节点
			vector<bool> check(n);
			q.push(srci);
			check[srci] = true;//入队列就标记
			while (!q.empty())
			{
				//控制一层一层出
				size_t sz = q.size();
				for (size_t i = 0; i < sz; ++i) //一层出完了再去走下一层
				{
					size_t front = q.front();//取队头
					q.pop();
					cout << front<< ":" << _vertexs[front] << " ";
					//然后让他的朋友进
					for (size_t i = 0; i < n; ++i)
						if (_matrix[front][i] != MAX_W && check[i] == false)
						{
							q.push(i);
							check[i] = true;
						}
				}
				cout << endl;
			}
		}
 
		void _DFS(size_t srci, vector<bool>& check) //下一个位置的起点以及需要一个标记数组
		{
			cout << srci << ":" << _vertexs[srci] << " " << endl; //先访问 然后标记为访问过
			check[srci] = true;
			for (size_t i = 0; i < _vertexs.size(); ++i)
				if (_matrix[srci][i] != MAX_W && check[i] == false)
					_DFS(i, check);
		}
 
 
		void DFS(const V& src) //需要有一个起点
		{
			size_t srci = GetVertexIndex(src);//找到起点的下标
			vector<bool> check(_vertexs.size()); //标记数组
			_DFS(srci, check);
			//如果是一个非连通图,可以在后面再进行一层检查check数组,然后对false的数组再进行一次访问
		}
 
 
		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) 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 << '*' << " ";
					else cout << _matrix[i][j] << " ";
				cout << endl;
			}
			cout << endl;
		}
 
	private:
		map<V, size_t> _IndexMap;    //建立顶点和下标之间的关系,方便我们根据顶点去找他的下标     比如两个顶点是否存在关系,就可以快速找到两个顶点的下标,然后去邻接矩阵看一下
		vector<V> _vertexs;			 // 顶点集合  
		vector<vector<W>> _matrix;   // 存储边集合的矩阵
	};

4.2 Prim算法

算法说明:


简单模拟现实:

cpp 复制代码
W Prim(Self& minTree, const V& src)  //Prim算法需要有一个起点     Prim适合稠密图 因为他的选取模式跟顶点有关系
{
	size_t n = _vertexs.size();
	//先初始化一下最小生成树
	minTree._vertexs = _vertexs;
	minTree._IndexMap = _IndexMap;
	minTree._matrix.resize(n);
	for (auto& e : minTree._matrix)  e.resize(n, MAX_W); //初始化成初始状态
	//  用一个set 或者是两个bool数组来帮助我们判环
	size_t srci = GetVertexIndex(src);
	set<size_t> inSet; //帮助我们存已经选过的顶点集合
	inSet.insert(srci);//先将起点丢进set中
	priority_queue<Edge, vector<Edge>, greater<Edge>> heap;//建小堆 
	for (size_t i = 0; i < n; ++i)//先将跟起点相连的所有边丢到优先级队列中(无差别入队列,但是多一层判断)
		if (_matrix[srci][i] != MAX_W)
			heap.push(Edge(srci, i, _matrix[srci][i]));
	//开始走选边的主逻辑
	W total = W();//统计总权重值
	while (inSet.size() < n && !heap.empty())
	{
		Edge min = heap.top();
		heap.pop();
		//看看两个点是否同时在set中
		if (inSet.find(min._dsti) == inSet.end())//必然是从srci出发的,所以我们只要保证dsti不在set中即可
		{
			cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] << ":" << _matrix[min._srci][min._dsti] << endl;
			//都不在set中,此时将边放到最小生成树中.
			minTree._AddEdge(min._srci, min._dsti, min._w);
			total += min._w;
			//然后将新丢进去的顶点的相连的边丢进优先级队列中
			for (size_t i = 0; i < n; ++i)//先将跟起点相连的所有边丢到优先级队列中
				if (_matrix[min._dsti][i] != MAX_W && inSet.find(i) == inSet.end()) //Set中有的就不要丢进去了
					heap.push(Edge(min._dsti, i, _matrix[min._dsti][i]));
			inSet.insert(min._dsti);
		}
		else
		{
			cout << "成环了所以不选:";
			cout << _vertexs[min._srci] << "-" << _vertexs[min._dsti] << ":" << _matrix[min._srci][min._dsti] << endl;
		}
	}
	//检测  因为该图可能是一个不连通的图,如果不连通的话就返回W的默认构造!
	if (inSet.size() == n) return total;
	else return W();
}

4.3 总结

Kruskal算法的时间复杂度为O(ElogE),其中E为边数。 E表示往优先级队列中丢边的过程,而logE表示优先级队列的调整。所以说边越少的复杂度就越低,所以说Kruskal算法适合稀疏图。

Prim算法的时间复杂度是O(N^2),遍历多久是取决于有多少个顶点,如果是稀疏图的话,他的边少,但是他的顶点也不一定少,所以说Prim算法更适合稠密图!!!

Prim算法得到的最小生成树是以一个顶点为起点的树形结构,而Kruskal算法得到的最小生成树是以边为基础的森林,需要进行额外的处理(依赖并查集判环)才能形成树。要根据不同的场景去选择。

五、最短路径


5.1 单源最短路径--Dijkstra算法

单源最短路径问题:

给定一个图G = ( V , E ) G=(V,E)G=(V,E),求源结点s ∈ V s∈Vs∈V到图中每个结点v ∈ V v∈Vv∈V的最短路径。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中,所以该算法使用的是贪心策略

算法说明:

Dijkstra算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路径的最短路径。

图解:

简单模拟现实:

cpp 复制代码
	//时间复杂度 O(N^2)  空间复杂度O(N)
	void Dijkstra(const V& src, vector<W>& dist, vector<int>& pPath) //src表示起点, dist表示最短路径  path表示父路径(可以帮助我们找到当前记录权值所经过的全部路径)
	{
		//先初始化一下我们的数组
		size_t srci = GetVertexIndex(src);
		size_t n = _vertexs.size();
		dist.resize(n, MAX_W);
		pPath.resize(n, -1);
		dist[srci] = W();//给一个初始值,方便第一次找到这个节点
		//需要设置一个bool数组帮助我们确定已经确定的最短路径的集合
		vector<bool> s(n);
		//先去我的各个路径去更新一下  一方面是去更新我们的dsti 一方面去找到最短的顶点u
		for (size_t i = 0; i < n; ++i) //一次走一个顶点 走n次
		{
			W min = MAX_W;//记录最小权重
			size_t u = srci;//记录最小顶点
			//先找到起点
			for (size_t j = 0; j < n; ++j)
				if (s[j] == false && dist[j] < min)
				{
					min = dist[j];
					u = j;
				}
			s[u] = true;//表示确定了点
					//以这个往外做松弛操作  更新一遍u连接的所有边  看看有没有可以更新的
			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 PrinrtShotPath(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) continue;
		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];
		cout << endl;
	}
}

5.2 单源最短路径--Bellman-Ford算法

Bellman---ford算法:

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

图解:

简单模拟现实:

cpp 复制代码
bool BellmanFord(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] = W();//给一个初始值,方便第一次找到这个节点
	bool flag = false;
	for (size_t k = 0; k < n; ++k) //按道理来说只需要进行n-1次必然就不需要松弛了
	{
		flag = false;//前置为false
		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])
				{
					dist[j] = dist[i] + _matrix[i][j];
					pPath[j] = i;
					flag = true;
				}
		if (flag == false)  break;//如果没有发生松弛 
	}
	//但是还得检查负权值回路
	//循环的最后一趟就是用来检查负权值的,如果最后一趟后flag仍为true 说明就有负权回路 应该return false   负权回路神仙难救
	return !flag;
}

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

说明:

其实多源最短路径按照Dijkstra算法或者是Bellman-Ford算法以所有点为起点也可以解决这个问题,但是前者不能解决负权值,后者的效率太低,因此都不够完美,所以才需要Floyd-Warshall算法来帮助我们解决多源最短路径问题,其实本质上是一种动态规划思想。

图解:

简单代码实现:

cpp 复制代码
	void FloydWarshall(vector<vector<W>>& dist, vector<vector<int>>& pPath)  //二维权值矩阵和父路径矩阵
	{
	  //初始化 权值和父路径矩阵
		size_t n = _vertexs.size();
		dist.resize(n);
		pPath.resize(n);
		for (size_t i = 0; i < n; ++i)
		{
			dist[i].resize(n,MAX_W);
			pPath[i].resize(n,-1);
		}
		//先将已有相连的边初始化到dist数组中
		for (size_t i = 0; i < n; ++i)
			for (size_t j = 0; j < n; ++j)
			{
				if (_matrix[i][j] != MAX_W)
				{
					dist[i][j] = _matrix[i][j];
					pPath[i][j] = i;
				}
				if (i == j) dist[i][j] = W();//方便和图对比,并且0不会影响其他路径
			}
		//利用k作为中间结点,去动态规划更新i->j的路径
		for (size_t k = 0; k < n; ++k)
		{
			for (size_t i = 0; i < n; ++i)
				for (size_t j = 0; j < n; ++j)
					if (dist[i][k] != MAX_W && dist[k][j] != MAX_W && dist[i][k] + dist[k][j] < dist[i][j])
					{
						dist[i][j] = dist[i][k] + dist[k][j];
						pPath[i][j] = pPath[k][j];//这里要填的是j的上一个邻接顶点
					}
 
			//打印权值和路径矩阵观察数据 ctrl+k ctrl+f 自动格式化
			// 打印权值和路径矩阵观察数据
			for (size_t i = 0; i < n; ++i)
			{
				for (size_t j = 0; j < n; ++j)
					if (dist[i][j] == MAX_W) printf("%3c", '*');
					else printf("%3d", dist[i][j]);
				cout << endl;
			}
			cout << endl;
 
			for (size_t i = 0; i < n; ++i)
			{
				for (size_t j = 0; j < n; ++j)
					printf("%3d", pPath[i][j]+1);//方便和图对比
				cout << endl;
			}
			cout << "=================================" << endl;
		}
	}

六、结束语

今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。

​​

相关推荐
.Vcoistnt10 分钟前
Codeforces Round 994 (Div. 2)(A-D)
数据结构·c++·算法·贪心算法·动态规划
ALISHENGYA35 分钟前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(实战训练三)
数据结构·c++·算法·图论
我码玄黄3 小时前
正则表达式优化之算法和效率优化
前端·javascript·算法·正则表达式
Solitudefire3 小时前
蓝桥杯刷题——day9
算法·蓝桥杯
三万棵雪松4 小时前
1.系统学习-线性回归
算法·机器学习·回归·线性回归·监督学习
Easy数模4 小时前
基于LR/GNB/SVM/KNN/DT算法的鸢尾花分类和K-Means算法的聚类分析
算法·机器学习·支持向量机·分类·聚类
2401_858286115 小时前
117.【C语言】数据结构之排序(选择排序)
c语言·开发语言·数据结构·笔记·算法·排序算法
thesky1234565 小时前
活着就好20241226
学习·算法
td爆米花5 小时前
C#冒泡排序
数据结构·算法·排序算法
chenziang15 小时前
leetcode hot100
算法·leetcode·职场和发展