数据结构 - 图

文章目录

一、图的基本概念

  1. 图是由顶点集合及顶点间的关系组成的一种数据结构:G = (V, E),其中: 顶点集合V = {x|x属于某个数据对象集}是有穷非空集合; E {(x,y)|x,y属于V}或者E = {<x, y>|x,y属于V && Path(x, y)}是顶点间关系的有穷集合,也叫做边的集合。 (x, y)表示x到y的一条双向通路,即(x, y)是无方向的;Path(x, y)表示从x到y的一条单向通路,即 Path(x, y)是有方向的。
  2. 顶点和边:图中结点称为顶点,第i个顶点记作vi。两个顶点vi和vj相关联称作顶点vi和顶点vj之间有一条边,图中的第k条边记作ek,ek = (vi,vj)或<vi,vj>。
  3. 有向图和无向图:在有向图中,顶点对<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>。
  4. 完全图:在有n个顶点的无向图中,若有n * (n-1)/2条边,即任意两个顶点之间有且仅有一条边,则称此图为无向完全图;在n个顶点的有向图中,若有n * (n-1)条边,即任意两个顶点之间有且仅有方向相反的边,则称此图为有向完全图。
  5. 邻接顶点:在无向图中G中,若(u, v)是E(G)中的一条边,则称u和v互为邻接顶点,并称边(u,v)依附于顶点u和v;在有向图G中,若<u, v>是E(G)中的一条边,则称顶点u邻接到v,顶点v邻接自顶点u,并称边<u, v>与顶点u和顶点v相关联。
  6. 顶点的度:顶点v的度是指与它相关联的边的条数,记作deg(v)。在有向图中,顶点的度等于该顶点的入度与出度之和,其中顶点v的入度是以v为终点的有向边的条数,记作indev(v);顶点v的出度是以v为起始点的有向边的条数,记作outdev(v)。因此:dev(v) = indev(v) + outdev(v)。注意:对于无向图,顶点的度等于该顶点的入度和出度,即dev(v) = indev(v) = outdev(v)。
  7. 路径:在图G = (V, E)中,若从顶点vi出发有一组边使其可到达顶点vj,则称顶点vi到顶点vj的顶点序列为从顶点vi到顶点vj的路径。
  8. 路径长度:对于不带权的图,一条路径的路径长度是指该路径上的边的条数;对于带权的图,一条路径的路径长度是指该路径上各个边权值的总和。
  9. 简单路径与回路:若路径上各顶点v1,v2,v3,...,vm均不重复,则称这样的路径为简单路径。若路径上第一个顶点v1和最后一个顶点vm重合,则称这样的路径为回路或环。
  10. 子图:设图G = {V, E}和图G1 = {V1,E1},若V1属于V且E1属于E,则称G1是G的子图。
  11. 连通图:在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1与顶点v2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图。
  12. 强连通图:在有向图中,若在每一对顶点vi和vj之间都存在一条从vi到vj的路径,也存在一条从vj到vi的路径,则称此图是强连通图。
  13. 生成树:一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的生成树有n个顶点和n-1条边

二、图的储存结构

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

1、邻接矩阵

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

注意:

(1)如果有权值的话,相连的边数组保存边的权值,不相连的边一般用无穷大表示。

(2)无向图的邻接矩阵是对称的,有向图则不是。

(3)邻接矩阵的优点是可以快速查找两个顶点是否相连,缺点是如果顶点多而边少,就会造成空间浪费。

代码实现:

bash 复制代码
// V - 顶点类型,W - 权值类型, MAX_W - 权值最大值, 
// Direction - 是否有向 true - 无向 false - 无向
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* vertexs, size_t n)
	{
		//初始化
		_vertexs.resize(n);
		_matrix.resize(n);
		for (int i = 0; i < n; i++)
		{
			//初始化矩阵
			_matrix[i].resize(n, MAX_W);
			//初始化映射关系
			_vertexs[i] = vertexs[i];
			_vIndexMap[vertexs[i]] = i;
		}
	}

	//获取映射下标
	size_t GetVertexIndex(const V& v)
	{
		//查找是否存在
		auto ret = _vIndexMap.find(v);

		//存在返回索引否则返回-1
		if (ret != _vIndexMap.end())
			return ret->second;
		else 
			cout<<"不存在顶点"<<endl;
		return -1;
	}

	void _AddEdge(size_t srci, size_t dsti, const W& w)
	{
		//默认有向
		_matrix[srci][dsti] = w;

		//无向
		if (Direction)
		{
			_matrix[dsti][srci] = w;
		}
	}

	//添加边
	void AddEdge(const V& src, const V& dst, const W& w)
	{
		//获取下标
		int srci = GetVertexIndex(src);
		int dsti = GetVertexIndex(dst);

		//判断是否存在
		if (srci >= 0 && dsti >= 0)
			_AddEdge(srci, dsti, w);
	}
	private:
	map<V, size_t> _vIndexMap;//元素与下标的映射
	vector<V> _vertexs; // 顶点集合
	vector<vector<W>> _matrix; //矩阵
}

下面其他算法均使用邻接矩阵实现。

2、邻接表

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

无向图邻接表存储:

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

有向图邻接表存储:

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

实现:

只实现出边表(一般只会用到)。

bash 复制代码
//顶点集合
	//W - 权值类型
	template<class W>
	struct LinkEdge
	{
		int _srcIndex;
		int _dstIndex;
		W _w;
		LinkEdge<W>* _next;
		LinkEdge(int srcIndex , int dstIndex , const W& w)
			: _srcIndex(srcIndex)
			, _dstIndex(dstIndex)
			, _w(w)
			, _next(nullptr)
		{}
	};

	//V - 顶点类型,W - 权值类型
	// Direction - 是否有向 true - 无向 false - 无向
	template<class V, class W, bool Direction = false>
	class Graph
	{
		typedef LinkEdge<W> Edge;
	public:
		Graph() = default;
		//初始化
		Graph(const V* vertexs, size_t n)
		{
			//初始化
			_linkTable.resize(n,nullptr);

			_vertexs.resize(n);
			for (int i = 0; i < n; i++)
			{
				//初始化映射关系
				_vertexs[i] = vertexs[i];
				_vIndexMap[vertexs[i]] = i;
			}
		}

		//获取下表
		size_t GetVertexIndex(const V& v)
		{
			//查找是否存在
			auto ret = _vIndexMap.find(v);

			//存在返回索引否则返回-1
			if (ret != _vIndexMap.end())
				return ret->second;
			else
				cout << "不存在该顶点" << endl;

			return -1;
		}

		//添加边
		void AddEdge(const V& src, const V& dst,  const W& w)
		{
			//获取映射下标
			int srci = GetVertexIndex(src);
			int dsti = GetVertexIndex(dst);

			if (srci == -1 || dsti == -1)
				return;

			//使用头插法
			//有向图
			Edge* srceg = new Edge(srci, dsti, w);
			srceg->_next = _linkTable[srci];
			_linkTable[srci] = srceg;

			//无向图
			if (Direction)
			{
				Edge* dsteg = new Edge(dsti, srci, w);
				dsteg->_next =  _linkTable[dsti];
				_linkTable[dsti] = dsteg;
			}
		}



	private:
		map<string, int> _vIndexMap;	//顶点与下标的映射关系
		vector<V> _vertexs; // 顶点集合
		vector<Edge*> _linkTable;	//邻接表
	};

三、图的遍历

1、广度优先遍历

通过一个顶点一层层不断往外扩。

怎么实现?

(1)通过一个数组记录顶点是否被遍历到。

(2)通过队列,每次将队列的顶点拿出,并将其指向的顶点入队,循环上述操作。

bash 复制代码
//广度优先遍历
void BFS(const V& src)
{
	//获取索引
	int srci = GetVertexIndex(src);

	//节点数
	int n = _vertexs.size();
	
	//标记数组
	vector<bool> vis(n);

	//通过队列遍历访问
	queue<int> q;
	q.push(srci);
	vis[srci] = true;

	while (!q.empty())
	{
		//一层一层遍历
		int sz = q.size();
		for (int i = 0; i < sz; i++)
		{
			//取队头元素
			int index = q.front();
			q.pop();
			cout << index << ": " << _vertexs[index]<<" | ";

			//将与队头元素连接且灭访问过的点进队并做标记
			for (int j = 0; j < n; j++)
			{
				if (vis[j] == false && _matrix[index][j] != MAX_W)
				{
					vis[j] = true;
					q.push(j);
				}
			}
		}
		cout << endl;
	}
}

2、深度优先遍历

怎么实现?

从一个顶点出发,找到下一个顶点,再次以这个顶点出发,重复上述操作,直到找不到下一个顶点就进行回溯。

bash 复制代码
void _DFS(int index, vector<bool>& vis)
{

	cout << index << ": " << _vertexs[index] << endl;

	//将index指向且没有访问过的节点进行标配并递归搜索
	for (int i = 0; i < _vertexs.size(); i++)
	{
		if (vis[i] == false && _matrix[index][i] != MAX_W)
		{
			vis[i] = true;
			_DFS(i, vis);
		}
	}
}

//深度优先搜索
void DFS(const V& v)
{	
	//标记数组
	vector<bool> vis(_vertexs.size());
	//获取v的索引
	int srci = GetVertexIndex(v);
	//进行遍历
	_DFS(srci, vis);

	//将剩下没有遍历到的节点遍历
	for (int i = 0; i < _vertexs.size(); i++)
	{
		if (vis[i] == false)
			_DFS(i, vis);
	}
}

四、最小生成树

1、概念

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

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

  1. 只能使用图中的边来构造最小生成树
  2. 只能使用恰好n-1条边来连接图中的n个顶点
  3. 选用的n-1条边不能构成回路 构造最小生成树的方法:Kruskal算法和Prim算法。这两个算法都采用了逐步求解的贪心策略。

2、Kruskal算法

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

核心:每次迭代时,选出一条具有最小权值,且两端点不在同一连通分量上的边,加入生成树。

上图来自算法导论。

怎么实现?

(1)使用一个结构体保存边的信息。

(2)使用优先队列(小的在队头)将所有边进队。

(3)开始挑边,在挑的过程中使用一个并查集(将选顶点和不选的顶点分成两个集合)判断是否成环,直到挑到n-1(n:顶点的个数)条边结束。

c 复制代码
//并查集
#include<iostream>
#include<vector>

using namespace std;

class UnionFindSet
{
public:
	UnionFindSet(size_t size)
		:_ufs(size, -1)
	{}

	// 给一个元素的编号,找到该元素所在集合的名称
	int FindRoot(int index)
	{
		int root = index;
		while (_ufs[root] >= 0)
		{
			root = _ufs[root];
		}
			
		while(_ufs[index] >= 0)
		{
			int p = _ufs[index];
			_ufs[index] = root;
			index = p;
		}
		return root;
	}
	
	//将两个元素合拼到同一个集合里
	bool Union(int x1, int x2)
	{
		int root1 = FindRoot(x1);
		int root2 = FindRoot(x2);
		if (root1 == root2)
			return false;

		//小的并到大的里面
		if(abs(_ufs[root1]) < abs(_ufs[root2]))
			swap(root1,root2);

		_ufs[root1] += _ufs[root2];
		_ufs[root2] = root1;
		return true;
	}

	// 数组中负数的个数,即为集合的个数
	size_t Count()const
	{
		size_t ret = 0;
		for (int i = 0; i < _ufs.size(); i++)
		{
			if (_ufs[i] < 0)
				ret++;
		}

		return ret;
	}

	//判断两个元素是否在同一个集合里
	bool IsGather(int x1, int x2)
	{
		return FindRoot(x1) == FindRoot(x2);
	}

private:
	std::vector<int> _ufs;
};

//W - 权值类型
template<class W>
struct Edge
{
	int _srci;
	int _dsti;
	W _w;
	Edge(int srci, int dsti, const W& w)
		: _srci(srci)
		, _dsti(dsti)
		, _w(w)
	{}

	//重载大于
	bool operator>( const Edge<W>& e) const
	{
		return _w > e._w;
	}
};


//最小生成树 -- Kruskal算法
W Kruskal(Self& minTree)
{
	//通过现有的图对minTree进行初始化
	int n = _vertexs.size();
	minTree._vertexs = _vertexs;
	minTree._vIndexMap = _vIndexMap;
	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 (int i = 0; i < n; i++)
	{
		for (int j = i; j < n; j++)
		{
			if (_matrix[i][j] != MAX_W)
			{
				pq.push(Edge(i, j, _matrix[i][j]));
			}
		}
	}

	//通过并查集来判环
	UnionFindSet ufs(n);

	//开始选边 -- 选n-1条
	int count = 0;//记录边的数
	W ret = W();//记录权值

	while (!pq.empty())
	{
		//取队头元素
		Edge e = pq.top();
		pq.pop();
		//只要两个点不同时在ufs中说明这条边可选
		if (!ufs.IsGather(e._srci, e._dsti))
		{
			//加入并查集
			ufs.Union(e._srci,e._dsti);
			//添加边
			minTree._AddEdge(e._srci,e._dsti,e._w);

			count++;
			ret += e._w;
		}
		
		//选完边结束
		if(count == n-1)
			break;
	}

	return ret;

}

3、Prim算法

上图来自算法导论。

怎么实现

(1)从一个顶点开始,将与其相连的边加入优先队列(小的在队头)。

(2)使用一个vector容器标记顶点是否被访问

(3)出队,通过被指向的边是否被标记来判环(该边的起点肯定已经被标记了,因为该边进队时就是选择被标记的点作为起点的),没被标记,选择,直到选择n-1(n:顶点数)条边。

c 复制代码
//W - 权值类型
template<class W>
struct Edge
{
	int _srci;
	int _dsti;
	W _w;
	Edge(int srci, int dsti, const W& w)
		: _srci(srci)
		, _dsti(dsti)
		, _w(w)
	{}

	//重载大于
	bool operator>( const Edge<W>& e) const
	{
		return _w > e._w;
	}
};
//最小生成树 -- Prim算法
W Prim(Self& minTree, const V& src)
{
	//通过现有的图对minTree进行初始化
	int n = _vertexs.size();
	minTree._vertexs = _vertexs;
	minTree._vIndexMap = _vIndexMap;
	minTree._matrix.resize(n);
	for (int i = 0; i < n; i++)
	{
		minTree._matrix[i].resize(n, MAX_W);
	}

	//标记点是否被选
	vector<int> vis(n);
	int srci = GetVertexIndex(src);
	vis[srci] = true;

	//使用优先队列保存当前点出去的边
	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]));
		}
	}

	W ret = W();//记录权值
	int count = 0;//记录当前边数
	

	while (!pq.empty())
	{
		//取出队头元素
		Edge e = pq.top();
		pq.pop();

		//判环 --只要当前边的e._dsti没被标记就行,因为该边是由e._srci点发出的,所以
		//e._srci肯定被标志了,不用判断
		if (vis[e._dsti] == false)
		{
			//修改标志
			vis[e._dsti] = true;
			//添加边
			minTree._AddEdge(e._srci, e._dsti, e._w);

			//将新加入的点出去的边添加到优先队列中
			for (int i = 0; i < n; i++)
			{
				if (_matrix[e._dsti][i] != MAX_W)
				{
					pq.push(Edge(e._dsti, i, _matrix[e._dsti][i]));
				}
			}

			ret += e._w;
			count++;
		}

		//选完了结束
		if (count == n - 1)
			break;
	}
	
	return ret;
}

五、最短路径问题

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

1、单源最短路径--Dijkstra算法

  1. 单源最短路径问题:给定一个图G = ( V , E ) G=(V,E)G=(V,E),求源结点s ∈ V s∈Vs∈V到图 中每个结点v ∈ V v∈Vv∈V的最短路径。Dijkstra算法就适用于解决带权重的有向图上的单源最短 路径问题,同时算法要求图中所有边的权重非负。一般在求解最短路径的时候都是已知一个起点和一个终点,所以使用Dijkstra算法求解过后也就得到了所需起点到终点的最短路径。
  2. 针对一个带权有向图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中,所 以该算法使用的是贪心策略。
  3. Dijkstra算法存在的问题是不支持图中带负权路径,如果带有负权路径,则可能会找不到一些路 径的最短路径。

上图来自算法导论。

c 复制代码
//src -- 开始的点 dist -- src到每个点的最短距离 
// parentPath -- 记录src到其他点的过程上的最近顶点 如:src -> k -> j j点记录的是k的下标
void Dijkstra(const V& src, vector<W>& dist, vector<int>& parentPath)
{
	//初始化
	int n = _vertexs.size();
	dist.resize(n, MAX_W);
	parentPath.resize(n, -1);

	//标记数组
	vector<bool> vis(n);

	//给dist数组srci位置赋最小值,方便第一次找到
	int srci = GetVertexIndex(src);
	dist[srci] = W();


	//n个顶点更新n次
	for (int i = 0; i < n; i++)
	{
		int u = 0;
		int min = MAX_W;
		//到srci最小的点
		for(int j = 0; j < n; j++)
		{
			if (vis[j] == false && min > dist[j])
			{
				u = j;
				min = dist[j];
			}
		}
		
		//标志
		vis[u] = true;
		
		//松弛操作
		//	更新u->其他点(srci->u->其他点)的dist
		for (int k = 0; k < n; k++)
		{
			if (vis[k] == false && _matrix[u][k] != MAX_W &&
				dist[k] > dist[u] + _matrix[u][k])
			{
				dist[k] = dist[u] + _matrix[u][k];
				parentPath[k] = u;
			}
		}
	}
}

2、单源最短路径--Bellman-Ford算法

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

上图来自算法导论。

c 复制代码
//src -- 开始的点 dist -- src到每个点的最短距离 
// parentPath -- 记录src到其他点的过程上的最近顶点 如:src -> k -> j j点记录的是k的下标
bool BellmanFord(const V& src, vector<W>& dist, vector<int>& parentPath)
{

	size_t n = _vertexs.size();
	size_t srci = GetVertexIndex(src);
	// vector<W> dist,记录srci-其他顶点最短路径权值数组
	dist.resize(n, MAX_W);
	// vector<int> parentPath 记录srci-其他顶点最短路径父顶点数组
	parentPath.resize(n, -1);

	// 先更新srci->srci为最小值,方便第一次找到
	dist[srci] = W();
	

	//最多更新n-1次(最坏的情况就是到另一个点需要更新n-1次)
	for (int k = 0; k < n-1; k++)
	{
		//标记,如果没有修改则完成查找最短路径
		bool flag = 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])
				{
					cout << _vertexs[i] << "->" << _vertexs[j] << ":" << _matrix[i][j] << " | ";
					dist[j] = dist[i] + _matrix[i][j];
					parentPath[j] = i;
					flag = true;
				}
			}
		}

		//全部都是最短路径了 - 结束
		if (flag == false)
			break;
	}

	//检查有没有负权回路
	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])
			{
				return false;
			}
		}
	}

	return true;
}

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个点最短路径,然后建立起转移方程,然后通过空间优化,优化掉最后一维度,变成一个最短路径的迭代算法,最后即得到所以点的最短路。

c 复制代码
//vvDist -- 记录全部的点到其他点的小权值 
//vvParentPath -- 记录全部点到其他点的过程上的最近顶点 如:src -> k -> j j点记录的是k的下标
void FloydWarShall(vector<vector<W>>& vvDist, vector<vector<int>>&
	vvParentPath)
{
	// 初始化
	size_t n = _vertexs.size();
	vvDist.resize(n);
	vvParentPath.resize(n);

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

	//将相连的边连在一起
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; j++)
		{
			if (_matrix[i][j] != MAX_W)
			{
				vvDist[i][j] = _matrix[i][j];
				vvParentPath[i][j] = i;
			}
			else if (i == j)
			{
				vvDist[i][j] = W();
				vvParentPath[i][j] = -1;
			}
		}
	}

	//将k作为中转点依次遍历
	for (int k = 0; k < n; k++)
	{
		for (int i = 0; i < n; i++)
		{
			for (int j = 0; j < n; j++)
			{
				//i ->( k )-> 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];
					vvParentPath[i][j] = vvParentPath[k][j];
				}
			}
		}
	}

}

六、拓扑排序

1、概念

拓扑排序是对有向无环图(DAG)顶点的一种排序。 在一个DAG中,如果存在一条有向边(u, v),那么在拓扑排序的结果中,顶点u会排在顶点v的前面。它主要用于解决任务调度、课程学习顺序等依赖关系问题(就是找做事情的先后顺序),通常可以用深度优先搜索(DFS)或 Kahn算法来实现拓扑排序。

注意:拓扑排序的顺序不唯一。

2、如何排序

以下是使用 Kahn 算法实现拓扑排序的步骤:


  1. 遍历有向图中的所有边,对于每条边(u, v),将顶点 v 的入度加一。
  2. 将入度为 0 的顶点加入队列。
    初始化一个队列,用于存储入度为 0 的顶点。遍历所有顶点,将入度为 0 的顶点加入队列。
  3. 进行拓扑排序。
    创建一个空列表,用于存储排序后的顶点。当队列不为空时,从队列中取出一个顶点 u。将顶点 u 加入排序后的列表中。遍历顶点 u 的所有邻接顶点 v,将顶点 v 的入度减一。如果顶点 v 的入度变为 0,则将其加入队列。
  4. 检查是否存在有向环。
    如果排序后的列表中顶点的数量与图中的顶点数量相同,则说明图中不存在有向环,拓扑排序成功。否则,说明图中存在有向环,无法进行拓扑排序。

3、实现

c 复制代码
//拓扑排序
//graph数组 -- 下标指向一个vector<int>,vector<int> - 该下标顶点指向的顶点
//如0 -> {1,2} ,在图中有两条边(0,1),(0,2)
vector<int> topologicalSort(vector<vector<int>>& graph) 
{
	//1、统计入度
	int n = graph.size();
	vector<int> in(n, 0);
	for (int i = 0; i < n; i++)
	{
		for (auto e : graph[i])
			in[e]++;
	}

	//2、将入度为0的进队列
	queue<int> q;
	for (int i = 0; i < n; i++)
	{
		if (in[i] == 0)
			q.push(i);
	}

	//3、拓扑排序
	vector<int> ret;
	while (!q.empty())
	{
		//出队
		int front = q.front();
		q.pop();

		//加入
		ret.push_back(front);

		//将front指向的入度减1
		for (auto e : graph[front])
		{
			in[e]--;
			//再次统计入度为0的顶点
			if (in[e] == 0)
				q.push(e);
		}
	}

	//4、判断是否存在有向环
	if (ret.size() < n)
		return {};

	return ret;
}

4、应用

题目:课程表

使用算法:拓扑排序

这题多了一步就是需要我们自己建图,遍历prerequisites使用vector<vector> 或者map<int,vector>,将图建(如顶点a指向其他顶点的集合)出来,其他步骤和上述代码差不多了。

代码实现:

c 复制代码
class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) 
    {
        //1、建图
        vector<vector<int>> graph(numCourses);
        int n = prerequisites.size();
        for(int i = 0; i < n; i++)
        {
            int a = prerequisites[i][0];
            int b = prerequisites[i][1];
            graph[b].push_back(a);
        }

        //统计入度
        vector<int> in(numCourses, 0);
        for (int i = 0; i < numCourses; i++)
        {
	        for (auto e : graph[i])
            {
                in[e]++;
            }
		        
        }

        //3、将入度为0的进队列
        queue<int> q;
        for (int i = 0; i < numCourses; i++)
        {
	        if (in[i] == 0)
		    q.push(i);
        }

        //4、拓扑排序
        while (!q.empty())
        {
	        //出队
	        int front = q.front();
	        q.pop();

	        //将front指向的入度减1
	        for (auto e : graph[front])
	        {
		        in[e]--;
		        //再次统计入度为0的顶点
		        if (in[e] == 0)
			    q.push(e);
	        }
        }

        //4、判断是否存在有向环
        for(int i = 0; i < numCourses; i++)
        {
           if(in[i]) return false;
        }

        return true;
    }
};
相关推荐
低保和光头哪个先来2 分钟前
牛客算法简单题(JS版)
javascript·算法
王老师青少年编程3 分钟前
CSP/信奥赛C++刷题训练:经典前缀和例题(2):洛谷P6568:水壶
c++·算法·前缀和·csp·信奥赛
CXDNW23 分钟前
【网络面试篇】TCP与UDP类
网络·笔记·tcp/ip·面试·udp
尤蒂莱兹6 小时前
qt的c++环境配置和c++基础【正点原子】嵌入式Qt5 C++开发视频
java·c++·qt
wingのpeterPen7 小时前
mac 上使用 cmake 构建包含 OpenMP 的项目
c++·macos
凡人的AI工具箱8 小时前
15分钟学 Go 第 21 天:标准库使用
开发语言·后端·算法·golang·1024程序员节
得鹿梦鱼、8 小时前
Qt/C++ 调用迅雷开放下载引擎(ThunderOpenSDK)下载数据资源
c++·qt·thunderopensdk
zhousiyuan05158 小时前
手撕FFT
算法
不做笔记的程序员不是好的码农8 小时前
C++-类与对象总结
开发语言·javascript·c++
战术摸鱼大师9 小时前
线性代数&群论应用:正逆运动学 & 变换矩阵
线性代数·算法·矩阵