数据结构---图(应该写的比较通俗易懂吧...)

目录

注意:第三,四,五部分的代码,结合  Design-in-cpp/design in cpp/Other data structures/Graph at main · wunianor/Design-in-cpp内的完整代码一起食用,效果更佳;文章内容较长,如有错误烦请指出,谢谢!!!

一.图的基本概念

1.1. 图的定义

1.2. 树与图的关系

1.3. 图的分类

1.4. 完全图

1.5. 邻接顶点

1.6. 顶点的度

1.7. 路径与路径长度,简单路径与回路

1.8. 子图

1.9. 连通图与强连通图

1.10. 生成树与最小生成树

1.11. 稀疏图和稠密图

二.图的存储方式

    2.1.邻接矩阵(第三,四,五节的内容都通过邻接矩阵实现)

      定义:

      示例:

      优点:

      缺点:

    2.2.邻接表

      定义:

      示例:

      优点:

      缺点:

三.图的遍历

3.1.广度优先遍历(bfs)

3.2.深度优先遍历(dfs)

四.图的最小生成树算法

4.1.Kruskal算法

    原理:

    画图举例说明:

    实现过程Q&A:

    实现代码:

4.2.Prim算法

    原理:

    画图举例说明:

    实现过程Q&A:

    实现代码:

五.图的最短路径算法

5.1.单源最短路径---Dijkstra算法(无法处理带负权边的图)

    原理:

    画图举例说明:

    实现过程Q&A:

    实现代码:

5.2.单源最短路径---Bellman_ford算法(可以处理带负权边的图)

    原理:

    画图举例说明:

    实现代码:(该算法还有一个名叫SPFA的优化,感兴趣的读者可以去了解,下面的代码没有实现)

5.3.多源最短路径---Floyd_warshall算法

    原理:

    实现代码:


注意:第三,四,五部分的代码,结合  Design-in-cpp/design in cpp/Other data structures/Graph at main · wunianor/Design-in-cpp内的完整代码一起食用,效果更佳;文章内容较长,如有错误烦请指出,谢谢!!!

一.图的基本概念

1.1. 图的定义

        图是由顶点(Vertex)集合和边(Edge)集合组成的一种数据结构,通常表示为G=(V,E),其中V是顶点的非空集合,E是边的集合,边是顶点的无序对(对于无向图)或有序对(对于有向图)。

        例如,在一个社交网络中,人可以看作顶点,人与人之间的朋友关系可以看作边。

1.2. 树与图的关系

        是一种特殊的图。树是一个无向连通图,其中任意两个顶点间有且仅有一条路径。它没有回路,并且有n - 1条边,其中n是顶点的个数。

        例如,家族族谱可以看作是一棵树,祖先为根节点,后代为叶子节点和中间节点。

1.3. 图的分类

        按边的方向分为无向图有向图。无向图中边没有方向,比如城市之间的公路连接(不考虑单行道),边(u,v)(v,u)是一样的。有向图中边是有方向的,例如网络中的网页链接,从网页A指向网页B的链接和从网页B指向网页A的链接是不同的概念。

        按边的权重分为有权图无权图有权图的边带有一个数值权重,比如在地图中城市之间的距离;无权图的边没有权重,或者可以认为权重都为1

1.4. 完全图

        对于无向完全图,每对不同的顶点之间都有一条边相连。一个有n个顶点的无向完全图有n(n - 1)/2条边。例如,在一个小组讨论中,如果每个人都要和其他所有人直接交流,这种关系可以用无向完全图来表示。

        对于有向完全图,每对不同的顶点之间都有两条有向边(方向相反),有n(n - 1)条边。

1.5. 邻接顶点

        对于无向图,如果(u,v)属于边集E,那么顶点uv互为邻接顶点

        在有向图中,如果存在有向边(u,v),那么称vu邻接顶点uv前驱顶点。        

        例如,在一个城市交通图中,如果有一条路连接城市A和城市B,那么城市A和城市B就是邻接顶点。

1.6. 顶点的度

        对于无向图,顶点v的度是与它相关联的边的数目,记作deg(v)。例如,在一个简单的三角形形状的无向图中,每个顶点的度都是2。在无向图中,所有顶点的度之和等于边数的两倍。这是因为每条边会对两个顶点的度各贡献1。例如,一个有5条边的无向图,所有顶点度之和为10。

        对于有向图,顶点v入度指向v的边的数目,记作indeg(v)出度v出发的边的数目,记作outdeg(v)顶点的度=入度+出度,即deg(v)=indeg(v)+outdeg(v)。在有向图中,所有顶点的入度之和=所有顶点的出度之和=边的数目。因为每条边有一个起点(贡献出度)和一个终点(贡献入度)。

1.7. 路径与路径长度,简单路径与回路

        路径:若从顶点u出发有一组边使其可以到达顶点v,则称顶点u到顶点v的顶点序列从顶点u到顶点v的路径.

        路径长度:对于无权图,一条路径的路径长度是指该路径上边的数量;对于有权图,一条路径的路径长度是指该路径上所有边的权重之和.

        简单路径:路径上各顶点v1,v2,...,vk均不重复,则称这样的路径为简单路径

        回路(环):起点和终点相同的简单路径

1.8. 子图

        设G=(V,E)是一个图,G'=(V',E')也是一个图,如果$V'\subseteq V$$E'\subseteq E$,则称$G'$$G$的子图。

        例如,从一个国家的交通地图中选取一个省的交通地图,这个省的交通地图就是整个国家交通地图的子图。

1.9. 连通图与强连通图

        在无向图中,如果任意两个顶点之间都存在一条路径,则称该图是连通图

        对于有向图,如果对于任意两个顶点$u$$v$,都存在$u$$v$的路径和从$v$$u$的路径,则称该有向图是强连通图

1.10. 生成树与最小生成树

        生成树:对于连通图$G=(V,E)$生成树$G$的一个子图,它是一棵树,包含$G$所有顶点,且边的数目为$n-1$,其中$n$是顶点的数目。

        例如,在一个电力网络中,要保证n个节点都能通电,且只使用n-1条电线连接,这些电线构成的图就是生成树。

        最小生成树:对于有权连通图,最小生成树是一棵生成树,它的所有边的权重之和最小.一个图的最小生成树不唯一.

        例如,在一个城市规划中,要在各个城市之间铺设通信电缆,使所有城市都能通信,并且总电缆长度最短,这个最短的电缆连接方案就是最小生成树。

1.11. 稀疏图和稠密图

        稀疏图是指边的数目相对顶点数目较少的图,通常当$|E|<<|V|^2$(对于无向图)时认为是稀疏图。

        稠密图是指边的数目相对顶点数目较多的图,接近完全图的情况可以看作是稠密图。

       

二.图的存储方式

    2.1.邻接矩阵(第三,四,五节的内容都通过邻接矩阵实现)

      定义

        邻接矩阵是表示图的一种常用方式。对于一个有n个顶点的图G=(V, E),它的邻接矩阵是一个(n \times n)的矩阵A

        如果G无向图,那么当顶点i和顶点j之间有边相连时A[i][j]=A[j][i] = 1(或者是等于边的权值);如果顶点i和顶点j之间没有边相连,则A[i][j]=A[j][i]=\infty

        如果G有向图,当存在从顶点i到顶点j的有向边时,A[i][j]=1(或者等于边的权值),否则A[i][j] = \infty

        顶点i自己到自己可以用A[i][i]=0表示.

      示例

        以一个简单的无向图为例,有顶点V = \{v_1,v_2,v_3\},边E=\{<v_1,v_2>,<v_1,v_3>\}.则它的邻接矩阵A为:\begin{bmatrix} 0 & 1 & 1\\ 1 & 0 & \infty\\ 1 & \infty & 0 \end{bmatrix},这里A[1][2]=A[2][1]=1表示顶点v_1v_2之间有边相连.

      优点

        ①.适合稠密图

        ②.O(1)时间复杂度判断两个顶点是否相连

      缺点

        ①.不适合稀疏图

        ②.不适合查找与一个顶点相连的所有边,需要O(n)的时间.

    2.2.邻接表

      定义

        邻接表是图的一种链式存储结构.对于图G=(V, E)中的每个顶点v_i\in V,都有一个链表,链表中的节点存储了与v_i相邻的顶点(和从v_i到其的权值).

        在有向图中,从顶点v_i出发的边对应的顶点(和边的权值)存储在v_i的邻接表中,这种邻接表也叫做出边表.入边表则存储到达v_i的边对应的顶点(和边的权值).

      示例

        对于无向图V = \{v_1,v_2,v_3\},边E=\{<v_1,v_2>,<v_1,v_3>\},它的邻接表可以表示为:

        v_1邻接链表:节点v_2→节点v_3

        v_2邻接链表:节点v_1

        v_3邻接链表:节点v_1

      优点

        ①.适合存储稀疏图

        ②.适合查找一个顶点连接出去(or连接进来)的边

      缺点

        ①.不适合确定两个顶点是否连接及其权值值

       

三.图的遍历

3.1.广度优先遍历(bfs)

        与树的bfs遍历差不多.

        从指定的顶点开始,通过队列实现bfs遍历.

        队列中存储每个顶点的下标.

        初始时,指定的顶点push进队列.

        然后每次取出队首元素i*,

        将v_i邻接顶点(没有遍历过的)入队列,同时将这些点标记为已遍历*.

        重复上述标*的过程,直到队列为空.

//  _vertexs是一个一维vector,可以通过下标找到顶点
//  _vertexs.size()为图中顶点的个数
//  get_vertex_index()->获取一个顶点在_vertexs数组内的下标
//  _matrix是一个二维vector,为图的邻接矩阵

//广度优先遍历
void bfs(const V& src)
{
	size_t srci = get_vertex_index(src);//获取顶点的下标
	queue<size_t> q;//队列中存储顶点的下标
	unordered_set<size_t> visited;//存储顶点的下标,表示哪些顶点已经遍历

	q.push(srci);
	visited.insert(srci);

	size_t level_count = 0;//记录当前遍历到第几层
	size_t level_size = q.size();//记录当前层有多少个顶点

	cout << "bfs:" << endl;

	while (!q.empty())
	{
		level_size = q.size();
		cout << " 第" << level_count << "层:";
		while (level_size--)
		{
			size_t front = q.front();
			q.pop();
			cout << _vertexs[front] << " ";
			for (size_t j = 0; j < _matrix[front].size(); ++j)
			{
				//如果_vertexs[front]顶点与_vertexs[j]有边连接 且 _vertexs[j]没有遍历过
				if (_matrix[front][j] != MAX_W && visited.count(j) == 0)
				{
					q.push(j);
					visited.insert(j);
				}
			}
		}
		++level_count;
		cout << endl;
	}

}

3.2.深度优先遍历(dfs)

        与树的dfs遍历差不多.

        从指定的顶点开始,通过递归实现dfs遍历.

//  _vertexs是一个一维vector,可以通过下标找到顶点
//  _vertexs.size()为图中顶点的个数
//  get_vertex_index()---获取一个顶点在_vertexs数组内的下标
//  _matrix是一个二维vector,为图的邻接矩阵


//深度优先遍历
void dfs(const V& src)
{
	size_t srci = get_vertex_index(src);
	vector<bool> visited(_vertexs.size(),false);//将已经遍历的顶点进行标记
	size_t depth = 0;//记录顶点的深度

	cout << "dfs:\n ";
	
	_dfs(srci, visited, depth);

}


//dfs子函数
void _dfs(size_t srci, vector<bool>& visited,size_t depth)
{
	//访问当前结点并标记
	cout << _vertexs[srci] << "(" << depth << ") ";
	visited[srci] = true;

	//寻找 与当前结点相邻并且未访问的点 访问
	for (size_t j = 0; j < _matrix[srci].size(); ++j)
	{
		if (_matrix[srci][j] != MAX_W && visited[j] == false)
		{
			_dfs(j, visited, depth + 1);
		}
	}
}

四.图的最小生成树算法

4.1.Kruskal算法

    原理:

        思想是贪心,

        将图中所有的边(除掉自环)按照权值存储在小堆内,

        从堆中依次取出每条边*,

        如果这条边不会与之前选择过的边形成环,那么就将这条边加入到最小生成树中*;

        如果会形成环,就抛弃*.

        重复上述标*的过程,直到遍历完所有的边.

        如果到最后最小生成树中有n-1条边(n为顶点个数),说明找到了这个图的最小生成树.

    画图举例说明:

    实现过程Q&A:

        1.怎么将边存储到小堆内?

                ①.创建一个结构体,有边的起点下标,终点下标,权值三个成员变量.

                ②.重载>运算符,用于堆内比较两条边的权值       

//内部类---边
struct Edge
{
	size_t _srci;//边的起点的下标
	size_t _desi;//边的终点的下标
	W _w;//权值

    //构造函数
	Edge(size_t srci,size_t desi,const W& w):
		_srci(srci),
		_desi(desi),
		_w(w)
	{}

	//用于排序(比较两条边的权值)
	bool operator>(const Edge& b) const 
	{
		return _w > b._w;
	}
};

          2.怎么判断会不会成环?

                ①.使用并查集,判断当前取出来的边的两端是否在同一个集合.如果在一个集合,说明选择当前的边会成环.

                ②.每次选择某条边后,这条边的两个顶点所在的集合进行合并.

    实现代码:

//  Self是typedef Graph<V, W, Direction, MAX_W>
//  V是顶点的类型,
//  W是边的权值的类型,
//  Direction表示这个图是有向图还是无向图
//  MAX_W作为两个顶点之间没有边的标识值
//  _vertexs是一个一维vector,可以通过下标找到顶点
//  _vertexs.size()为图中顶点的个数
//  Edge是边类
//  _matrix是一个二维vector,为图的邻接矩阵
//  add_edge_by_index()函数---通过顶点的下标添加边


//最小生成树Kruskal算法(贪心+排序(堆)+并查集)
W kruskal(Self& min_tree,bool debug = false)
{
	priority_queue<Edge, vector<Edge>, greater<Edge>> minpq;//小堆,按照边的权值来排序

	//找到所有的边
	for (size_t i = 0; i < _matrix.size(); ++i)
	{
		for (size_t j = (Direction == false ? i+1 : 0) ; j < _matrix[i].size(); ++j)
		{
			if (i == j) continue;//如果这条边的两个顶点是同一个顶点

			if (_matrix[i][j] != MAX_W)
			{
				minpq.push(Edge(i,j,_matrix[i][j]));
			}
		}
	}

	size_t min_tree_edge_size = 0; //最小生成树的边的条数
	W min_tree_total_w = W();      //最小生成树的总权值
	UnionFindSet ufs(_vertexs.size()); //并查集,记录哪些点是一个集合的

	//按照权值从小到大开始选边
	while (!minpq.empty())
	{
		Edge edge = minpq.top();
		minpq.pop();

		//如果这条边的两个顶点不在一个集合中(即不会成环)
		if (ufs.In_same_set(edge._srci, edge._desi) == false)
		{
			if (debug)//debug模式下打印选择的边
			{
				cout << "Selected Edge:" << _vertexs[edge._srci] << "->" << _vertexs[edge._desi] << "(w:" << edge._w << ")" << endl;
			}

			min_tree.add_edge_by_index(edge._srci, edge._desi, edge._w);//将这条边添加到最小生成树中
			ufs.Union(edge._srci, edge._desi);//合并集合
			min_tree_total_w += edge._w;
			++min_tree_edge_size;
		}
		else if (debug)//debug模式下打印未选择的边
		{
			cout << "Unselected Edge:" << _vertexs[edge._srci] << "->" << _vertexs[edge._desi] << "(w:" << edge._w << ")" << endl;
		}
	}

	//没找到最小生成树
	if (min_tree_edge_size != _vertexs.size() - 1)
	{
		return W();
	}

	//找到了最小生成树
	return min_tree_total_w;
}

4.2.Prim算法

    原理:

        思想是贪心,

        定义两个集合X,Y,

        集合X是已经在最小生成树内的顶点,

        集合Y是还未加入最小生成树内的顶点,

        初始时随便选取一个点(也可以在调用prim算法函数时传入一个点)在集合X内,其余点在集合Y内.

        然后在所有一端是X的元素,一端是Y的元素的*,

        找出权值最小的那条边*,

        将这条边加入到最小生成树*,

        同时将这条边的那个在集合Y的顶点从集合Y移动到集合X*.

        重复上述标*的过程,直到遍历完所有能遍历的顶点.

        如果最小生成树内有n-1条边(n为顶点个数),说明找到了这个图的最小生成树.

    画图举例说明:

    实现过程Q&A:

        1.如何定义集合X和Y?

                ①.使用一个一维vector<bool>,每个下标对应一个顶点,如果该顶点在X集合内,标记为true;在Y集合内标记为false.

        2.如何找到所有一端是X的元素,一端是Y的元素的边,并且找出权值最小的那条边?

                ①.当一个顶点加入X集合时,将一端为它,一端为Y集合的元素的所有边加入到小堆(按照边的权值比较)中(这一步小堆的代码可以参考Kruskal算法的实现过程部分)

                ②.从小堆中取出边时,需要判断这条边的一端是否在X集合内,一端是否在Y集合内.如果是,就将这条边加入到小堆中,否则从小堆中取下一条边.

    实现代码:

//最小生成树prim算法

//  Self是typedef Graph<V, W, Direction, MAX_W>
//  V是顶点的类型,
//  W是边的权值的类型,
//  Direction表示这个图是有向图还是无向图
//  MAX_W作为两个顶点之间没有边的标识值
//  _vertexs是一个一维vector,可以通过下标找到顶点
//  _vertexs.size()为图中顶点的个数
//  Edge是边类
//  _matrix是一个二维vector,为图的邻接矩阵
//  add_edge_by_index()函数---通过顶点的下标添加边
//  get_vertex_index()函数---获取一个顶点在_vertexs数组内的下标


W prim(Self& min_tree,const V& src,bool debug = false)
{
	size_t srci = get_vertex_index(src);

	vector<bool> X(_vertexs.size(),false); //已经加入最小生成树的顶点 在X中标记为true,否则为false
	X[srci] = true;

	
	W min_tree_total_w = W(); //最小生成树的总权
	size_t min_tree_edge_size = 0;//当前最小生成树边的数量
	priority_queue<Edge, vector<Edge>, greater<Edge>> minpq;//小堆,存边

	//将起点为src,终点为X中被标记成false的顶点 的边push进小堆
	for (size_t j = 0; j < _matrix[srci].size(); ++j)
	{
		if (_matrix[srci][j] != MAX_W && X[j] == false)
		{
			minpq.push(Edge(srci, j, _matrix[srci][j]));
		}
	}

	while (!minpq.empty())
	{
		Edge min_edge = minpq.top();
		minpq.pop();


		//判断是否会成环 
		//(取出的边的起点是否已加入最小生成树(标记成true),终点是否没有加入最小生成树(标记为false))
		if (X[min_edge._srci] == true && X[min_edge._desi] == false)
		{
			min_tree.add_edge_by_index(min_edge._srci, min_edge._desi, min_edge._w);
			min_tree_total_w += min_edge._w;
			++min_tree_edge_size;

			if (debug)//debug模式下打印选择的边
			{
				cout << "Selected Edge:" << _vertexs[min_edge._srci] << "->" << _vertexs[min_edge._desi] << "(w:" << min_edge._w << ")" << endl;
			}

			X[min_edge._desi] = true;//将min_edge._desi标记成true(已加入最小生成树)
				
			//将起点为min_edge._desi,终点为在X被标识成false的顶点 的边push进小堆
			for (size_t j = 0; j < _matrix[min_edge._desi].size(); ++j)
			{
				if (_matrix[min_edge._desi][j] != MAX_W && X[j] == false)
				{
					minpq.push(Edge(min_edge._desi, j, _matrix[min_edge._desi][j]));
				}
			}
		}
		else if (debug)//debug模式下打印未选择的边
		{
			cout << "Unselected Edge:" << _vertexs[min_edge._srci] << "->" << _vertexs[min_edge._desi] << "(w:" << min_edge._w << ")" << endl;
		}
	}

	//如果最小生成树的边不是 顶点数-1 条
	if (min_tree_edge_size != _vertexs.size() - 1)
	{
		return W();
	}

	return min_tree_total_w;
}

  

五.图的最短路径算法

        单源最短路径求从一个点出发,到达其他点的最短路径

        多源最短路径任意两点之间的最短路径

5.1.单源最短路径---Dijkstra算法(无法处理带负权边的图)

    原理:

        思想是贪心,只能处理不带负权边的图,

        将图中的所有顶点分为三个集合,

        分别为已经确定最短路径的顶点的集合X,没有确定最短路径但确定可以到达的顶点集合Y,暂未确定可以到达的顶点集合Z,

        初始时,起始顶点(src)在集合X内,其他顶点在集合Z内;并且使用一个一维vector数组dist_w,用于存储src到每个点的最短路径权值,数组dist_w内的值全部初始化成不可达的标识值,然后dist_w[src的下标]=0;再设置一个cur变量,cur=起始顶点的下标.

        随后对cur指向的点进行松弛更新操作*,

        更新与cur指向的点相邻且不在X集合内的所有顶点的最短路径权值(被更新最短路径的顶点可以认为在Y集合内)*,

        更新完后,找到Y集合内最短路径权值最小的那个点,这个点转移到集合X,同时让cur=这个点的下标,作为下一次进行松弛更新的顶点*.

        重复上述标*的操作,直到找不到下一次能进行松弛更新的顶点为止(此时,已经更新完所有src能到达的顶点的最短路径权值).

        在上述操作中,还可以增加一个一维数组p_path,存储从src到这个点的最短路径(eg:src->a->b->c->destination)中倒数第二个顶点的下标(如示例中的c的下标).在算法运行的过程中不断地维护p_path数组.

    画图举例说明:

    实现过程Q&A:

        1.真的需要实现上述的X,Y,Z三个集合吗?如果不需要,我怎么确定某个顶点是否确定了最短路径?

                不需要真的实现上述的X,Y,Z三个集合(这三个集合只是为了便于理解);只需要用一个一维bool数组即可,确定了最短路径的顶点标记为true,否则即为false.

        2.怎么对cur指向的点进行松弛更新操作?

                对于与cur指向的点相邻且未在X集合内的顶点V_i,

                如果dist_w[V_i]=不可达的标识值 或者 dist_w[cur]+cur与V_i相连的边的权值<dist_w[V_i],

                就更新dist_w[V_i].

    实现代码:

        (如果图是用邻接表存储的,可以使用寻找下一个进行松弛操作的顶点的过程进行优化)

/*
	最短路径Dijkstra算法--->时间复杂度:O(n^2),空间复杂度O(n),其中n为顶点个数
	思想是贪心
	dist_w存src到各个点的最短路径的权值
	p_path存从src到这个点的最短路径(eg:src->a->b->c->destination)中倒数第二个顶点的下标(如示例中的c的下标),
	p_path可以倒推从src到各个点的最短路径

    V为顶点的类型,
    W为边的权值的类型,
    MAX_W作为两个顶点之间没有边(or不能到达)的标识值
    get_vertex_index()函数---获取一个顶点在_vertexs数组内的下标
    _vertexs是一个一维vector,可以通过下标找到顶点
    _vertexs.size()为图中顶点的个数
    _matrix是一个二维vector,为图的邻接矩阵
*/
void dijkstra(const V& src, vector<W>& dist_w, vector<size_t>& p_path)
{
	dist_w.resize(_vertexs.size(), MAX_W);
	p_path.resize(_vertexs.size(), -1);
	vector<bool> hash(_vertexs.size(), false);//最短路径已经确定的顶点标记为true,否则为false

	size_t srci = get_vertex_index(src);
	dist_w[srci] = W();
	p_path[srci] = srci;
	hash[srci] = true;

	size_t cur = srci;//当前要进行松弛更新的点


	while (true)
	{
		//松弛更新操作
		for (size_t j = 0; j < _matrix[cur].size(); ++j)
		{
			if (_matrix[cur][j] != MAX_W && //如果cur顶点能到j顶点(cur顶点和j顶点之间有边连接)
				hash[j] == false &&         //并且 src->j顶点的最短路径还没有确定
				(dist_w[j]==MAX_W || dist_w[cur]+_matrix[cur][j] < dist_w[j]))//并且 到j顶点的权值为MAX_W or src->cur + cur->j < src->j
			{
				dist_w[j] = dist_w[cur] + _matrix[cur][j];
				p_path[j] = cur;
			}
		}

		//寻找被hash标记为false 且 到达它的权值最小的顶点,用于迭代cur
		size_t next = -1;//下一个要进行松弛更新的点
		for (size_t i = 0; i < _vertexs.size(); ++i)
		{
			if (hash[i] == false &&                       //如果i顶点标记为false,
				dist_w[i] != MAX_W &&                     //并且目前判断src能到i顶点,
				(next == -1 || dist_w[i] < dist_w[next])) //并且next顶点不存在 or 到达i顶点的权值小于到达next顶点的权值
			{
				next = i;
			}
		}

		if (next != -1)//如果找到了,迭代cur,并在hash中将cur指向的顶点标记为true
		{
			cur = next;
			hash[cur] = true;
		}
		else //如果没找到,说明src到所有点(可到达的点)的最短路径已经更新完毕
		{
			break;
		}
	}
}

5.2.单源最短路径---Bellman_ford算法(可以处理带负权边的图)

    原理:

        思想是暴力,可以处理带负权边的图,可以判断图中是否有负权回路(回路上所有边的权值和为负数), 

        一维数组dist_w存储起始顶点src到每个顶点的最短路径权值,dist_w数组内所有元素全部初始化为不可达的标识值,然后dist_w[起始顶点src的下标]=0

        一维数组p_path存储从src到这个点的最短路径(eg:src->a->b->c->destination)中倒数第二个顶点的下标(如示例中的c的下标),p_path数组内所有元素全部初始化为-1,然后p_path[起始顶点src的下标]=起始顶点src的下标

        所有边进行n轮遍历(n为顶点的个数),

        在每一轮中,如果顶点v_i与顶点v_j有边相连,dist_w[v_j]=不可达标识值 or dist_w[v_i]+v_i\rightarrow v_j<dist_w[v_j],就更新dist_w[v_j]=dist_w[v_i]+v_i\rightarrow v_j.

        最后再进行一轮遍历,如果发现还能更新,说明有负权回路.

    画图举例说明:

    实现代码:(该算法还有一个名叫SPFA的优化,感兴趣的读者可以去了解,下面的代码没有实现)

//bellman_ford算法
//思想是暴力
//可以求带负权图的最短路径问题,可以检测负权回路
//时间复杂度O(n^3),空间复杂度O(n)

//  V为顶点的类型,
//  W为边的权值的类型,
//  MAX_W作为两个顶点之间没有边(or不能到达)的标识值
//  get_vertex_index()函数---获取一个顶点在_vertexs数组内的下标
//  _vertexs是一个一维vector,可以通过下标找到顶点
//  _vertexs.size()为图中顶点的个数
//  _matrix是一个二维vector,为图的邻接矩阵
bool bellman_ford(const V & src, vector<W>&dist_w, vector<size_t>&p_path)
{
	dist_w.resize(_vertexs.size(), MAX_W);
	p_path.resize(_vertexs.size(), -1);

	//初始化dist_w[srci]和p_path[srci]
	size_t srci = get_vertex_index(src);
	dist_w[srci] = W();
	p_path[srci] = srci;

	//进行n轮更新(src到某一个点最多经过n-2个点)
	//第n+1轮(round==_vertexs.size())是用来检测是否存在负权回路
	for (size_t round = 0; round <= _vertexs.size(); ++round)
	{
		bool update = false;//检查是否进行了松弛更新

		//每一轮找到邻接矩阵中的所有边,对src到所有点的最短路径进行更新
		for (size_t i = 0; i < _matrix.size(); ++i)
		{
			for (size_t j = 0; j < _matrix[i].size(); ++j)
			{
				if (_matrix[i][j] != MAX_W &&                 //如果i可以到j,
					(dist_w[j] == MAX_W ||                    //并且 distw[j]等于标识值(代表不可到达)
				     dist_w[i] + _matrix[i][j] < dist_w[j]))  //     或者 src->i + i->j < src->j
				{   //进行松弛更新
					update = true;
					dist_w[j] = dist_w[i] + _matrix[i][j];
					p_path[j] = i;
				}
			}
		}

		//如果没更新,break
		if (update == false)
		{
			break;
		}

		//如果第n+1轮进行了松弛更新,说明存在负权回路,返回false
		if (round == _vertexs.size() && update == true)
		{
			return false;
		}
	}

	return true;
}

5.3.多源最短路径---Floyd_warshall算法

    原理:

        原理是动态规划,下图是它的定义(摘自OI-wiki).

        下面说说我的理解:

        假设顶点v_i到顶点v_j有路径,那么先初始化成这样:v_i\rightarrow v_j,

        随后遍历图中剩余的n-2个顶点(n为顶底个数),对于剩余n-2个顶点中的每个顶点v_k:

                如果v_i通过v_kv_j的路径权值 < 当前v_i不通过v_kv_j的路径权值,v_i\rightarrow v_k + v_k\rightarrow v_j <v_i\rightarrow v_j,

                就将v_k加入v_iv_j的路径中,即v_i\rightarrow ...\rightarrow v_k\rightarrow ...\rightarrow v_j

        图中的任意两点进行该操作,即可得到任意两点的最短路径.

    实现代码:

//floyd_warshall算法(多源最短路径)
//求任意两个点之间的最短路径
//原理是动态规划
//dist_i_j存i到j点的最短路径的权值
//p_path_i_j存i->j的最短路径(eg:i->a->b->c->j)上 j的上一个顶点的下标(例子中是c的下标)

//  V为顶点的类型,
//  W为边的权值的类型,
//  MAX_W作为两个顶点之间没有边(or不能到达)的标识值
//  get_vertex_index()函数---获取一个顶点在_vertexs数组内的下标
//  _vertexs是一个一维vector,可以通过下标找到顶点
//  _vertexs.size()为图中顶点的个数
// _matrix是一个二维vector,为图的邻接矩阵

void floyd_warshall(vector<vector<W>>& dist_i_j,vector<vector<size_t>>& p_path_i_j,bool debug = false)
{
	//初始化dist_i_j和p_path_i_j
	dist_i_j.resize(_vertexs.size());
	p_path_i_j.resize(_vertexs.size());
	for (size_t i = 0; i < _vertexs.size(); ++i)
	{
		dist_i_j[i].resize(_vertexs.size(), MAX_W);
		dist_i_j[i][i] = W();

		p_path_i_j[i].resize(_vertexs.size(), -1);
		p_path_i_j[i][i] = i;
	}

	//将邻接矩阵中的所有边先添加到dist_i_j内
	for (size_t i = 0; i < _matrix.size(); ++i)
	{
		for (size_t j = 0; j < _matrix[i].size(); ++j)
		{
			if (_matrix[i][j] != MAX_W)
			{
				dist_i_j[i][j] = _matrix[i][j];
				p_path_i_j[i][j] = i;
			}
		}
	}

	//核心内容
	for (size_t k = 0; k < _vertexs.size(); ++k)
	{
		for (size_t i = 0; i < _vertexs.size(); ++i)
		{
			for (size_t j = 0; j < _vertexs.size(); ++j)
			{
				if (dist_i_j[i][k] != MAX_W &&                         //如果顶点i可以到顶点k
					dist_i_j[k][j] != MAX_W &&                         //并且顶点k可以到顶点j
					dist_i_j[i][k] + dist_i_j[k][j] < dist_i_j[i][j])  //并且i->k + k->j < i->j
				{
					dist_i_j[i][j] = dist_i_j[i][k] + dist_i_j[k][j];
					p_path_i_j[i][j] = p_path_i_j[k][j];
				}
			}
		}


		if (debug)//在debug模式下
		{
			// 打印权值观察数据
			for (size_t i = 0; i < dist_i_j.size(); ++i)
			{
				for (size_t j = 0; j < dist_i_j[i].size(); ++j)
				{
					if (dist_i_j[i][j] == MAX_W)
					{
						cout << setw(5) << "*";
						//printf("%4c", '*');
					}
					else
					{
						cout << setw(5) << dist_i_j[i][j];
						//printf("%4d", dist_i_j[i][j]);
					}


					/*cout << " path:";

					vector<V> path_i_j;
					size_t parent = j;
					while (parent != i)
					{
						path_i_j.push_back(_vertexs[parent]);
						parent = p_path_i_j[i][parent];
					}
					path_i_j.push_back(_vertexs[i]);

					reverse(path_i_j.begin(), path_i_j.end());

					for (auto& vertex : path_i_j)
					{
						if (vertex != _vertexs[j])
						{
							cout << vertex << "->";
						}
						else
						{
							cout << vertex << endl;
						}
					}*/

				}
				cout << endl;
			}
			cout << "---------------------------------------------" << endl;
		}
	}
	
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值