数据结构与算法(8)--图

Ch8 图


0x01 图的基本概念和术语


是由顶点集合及顶点间的关系集合组成的一种数据结构。

一些基本术语

(1)顶点和边:图中的结点称作顶点,图中的第i个顶点记做vi。两个顶点vi和vj相关联称作顶点vi和vj之间有一条边,图中的第k条边记做ek,ek =(vi,vj)或<vi,vj>。
(2)有向图和无向图:在有向图中,顶点对<x ,y>是有序的,顶点对<x,y>称为从顶点x到顶点y的一条有向边,有向图中的边也称作;在无向图中,顶点对(x,y)是无序的,顶点对(x,y)称为与顶点x和顶点y相关联的一条边,即无向图中(x,y)和(y,x)其实是一条边。
(3)完全图:在有n个顶点的无向图中,若有n(n-1)/2条边,即任意两个顶点之间有且只有一条边,则称此图为无向完全图;在有n个顶点的有向图中,若有n(n-1)条边,即任意两个顶点之间有且只有方向相反的两条边,则称此图为有向完全图
(4)邻接顶点:在无向图G中,若(u,v)是E(G)中的一条边,则称u和v互为邻接顶点,并称边(u,v)依附于顶点u和v;在有向图G中,若<u,v>是E(G)中的一条边,则称顶点u邻接到顶点v,顶点v邻接自顶点u,并称边<u,v>和顶点u和顶点v相关联。
(5)顶点的度:顶点v的度是与它相关联的边的条数,记作TD(v)。
(6)路径:在图G=(V,E)中,若从顶点vi出发有一组边使可到达顶点vj,则称顶点vi到顶点vj的顶点序列为从顶点vi到顶点vj的路径。
(7):有些图的边附带有数据信息,这些附带的数据信息称为权。带权的图也称作网络或网。
(8)路径长度:对于不带权的图,一条路径的路径长度是指该路径上的边的条数;对于带权的图,一条路径的路径长度是指该路径上各个边权值的总和。
(9)子图:某个图的边和顶点都包含于另一个图,这个图就是另一个图的子图。
(10)连通图强连通图:在无向图中,若从顶点vi到顶点vj有路径,则称顶点vi和顶点vj是连通的。如果图中任意一对顶点都是连通的,则称该图是连通图。
在有向图中,若对于任意一对顶点vi和顶点vj(vi≠vj)都存在路径,则称图G是强连通图。
(11)生成树:一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的生成树有n个顶点和n-1条边。
(12)简单路径和回路:若路径上各顶点v1,v2,…,vm,互不重复,则称这样的路径为简单路径;若路径上第一个顶点v1与最后一个顶点vm重合,则称这样的路径为回路或环 。

图的操作集合

初始化、插入顶点、插入边、删除边、删除顶点、第一个邻接节点、下一个邻接节点、遍历。


0x02 图的存储结构


主要有邻接矩阵邻接表两种。

邻接矩阵

邻接矩阵中元素aij=1表示存在vi到vj的边。下面是图邻接矩阵例子。
图邻接矩阵.png
对于带权图,aij的值可以为权重,其他为∞。

图邻接表存储结构

当图的边数少于顶点个数且顶点个数值较大时,图的邻居矩阵就成了稀疏矩阵的存储问题,此时,使用邻接表可能更为有效。

图的邻接表.png
数组的data域存储图的顶点信息,sorce域存储该顶点在数组中的下标序号,adj域为该顶点的邻接顶点单链表的头指针。第i行单链表中的dest域存储所有起始顶点为vi的邻接顶点vj在数组中的下标序号,next域为单链表中下一个邻接顶点的指针域。如果是带权图,单链表中需再增加cost域,用来存储边<vi,vj>的权值wij。

具体的实现可参看书上伪代码或者源码。


0x03 图的遍历


定义:从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历,它是图的基本运算
遍历实质:找每个顶点的邻接点的过程。
图的特点:图中可能存在回路,且图的任一顶点都可能与其它顶点相通,在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点。

如何避免重复访问?
解决思路:设置辅助数组 visited [n ],用来标记每个被访问过的顶点。
初始状态为0
i 被访问,改 visited [i]为1,防止被多次访问

常用的遍历方法有深度优先搜索和广度优先搜索。

深度优先搜索DFS

基本思想是模仿树的先根遍历过程
简单归纳:

-访问起始点v;
-若v的第1个邻接点没访问过,深度遍历此邻接点;
-若当前邻接点已访问过,再找v的第2个邻接点重新遍历。

具体实现时可以建立辅助数组,标识某个邻接点是否被访问过。
DFS.png
若不给定存储结构,深度优先遍历的序列不唯一。因为哪个顶点是第一邻接点未确定。给定存储结构后,深度优先遍历的结果是唯一的。

广度优先搜索BFS

通图的广度优先遍历算法为:

图的广度优先遍历算法是一个分层搜索的过程,需要一个队列以保持访问过的顶点的顺序,以便按访问过的顶点的顺序来访问这些顶点的邻接顶点。连通图的广度优先遍历算法为:
(1)访问初始顶点v并标记顶点v为已访问;
(2)顶点v入队列;
(3)当队列非空时则继续执行,否则算法结束;
(4)出队列取得队头顶点u;
(5)查找顶点u的第一个邻接顶点w;
(6)若顶点u的邻接顶点w不存在,则转到步骤(3),否则循环执行,
(6.1)若顶点w尚未被访问则访问顶点w并标记顶点w为已访问;
(6.2)顶点w入队列;
(6.3)查找顶点u的w邻接顶点后的下一个邻接顶点w,转到步骤(6)。

非连通图的遍历

对于非连通图,从图的任意一个顶点开始深度或广度优先遍历并不能访问图中的所有顶点。只能访问和初始顶点连通的所有顶点
但是,每一个顶点都作为一次初始顶点进行深度优先遍历或广度优先遍历,并根据顶点的访问标记来判断是否需要访问该顶点,就一定可以访问非连通图中的所有顶点。

深度优先遍历代码实现(重要)

连通图
邻接矩阵存储结构图类的深度优先遍历成员函数如下:
void AdjMWGraph::DepthFirstSearch(const int v, int visited[], 
void Visit(VerT item))
//连通图G以v为初始顶点序号、访问操作为Visit()的深度优先遍历
//数组visited标记了相应顶点是否已访问过,0表示未访问,1表示已访问
{
	Visit(GetValue(v));			//访问该顶点
	visited[v] = 1;				//置已访问标记

	int w = GetFirstNeighbor(v);		//取第一个邻接顶点
	while(w != -1)				//当邻接顶点存在时循环
	{
		if(! visited[w]) 
DepthFirstSearch(w, visited, Visit);                //递归
		w = GetNextNeighbor(v, w);	//取下一个邻接顶点
	}
}
非连通图
void AdjMWGraph::DepthFirstSearch(void Visit(VerT item))
//非连通图G访问操作为Visit()的深度优先遍历
{
	int *visited = new int[NumOfVertices()];

	for(int i = 0; i < NumOfVertices(); i++) 
visited[i] = 0;								//初始化访问标记

	for(i = 0; i < NumOfVertices(); i++)
		if(! visited[i]) 
DepthFirstSearch(i, visited, Visit);	//深度优先遍历

	delete []visited;
}

广度优先遍历代码实现(重要)

连通图BFS
#include "SeqQueue.h"		//包含静态数组结构的顺序队列类
void AdjMWGraph::BroadFirstSearch(const int v, int visited[], 
void Visit(VerT item))
//连通图G以v为初始顶点序号、访问操作为Visit()的广度优先遍历
//数组visited标记了相应顶点是否已访问过,0表示未访问,1表示已访问
{
	VerT u, w;
	SeqQueue queue;				//定义队列

	Visit(GetValue(v));				//访问该顶点
	visited[v] = 1;					//置已访问标记

	queue.Append(v);				//顶点v入队列
	while(queue.NotEmpty())			//队列非空时循环
	{
		u = queue.Delete();			//出队列
		w = GetFirstNeighbor(u);         //取顶点u的第一个邻接顶点
		                             while(w != -1)			//邻接顶点存在时
		{
		if(!visited[w])		//若该顶点没有访问过
		{
		 	Visit(GetValue(w));	//访问该顶点
			visited[w] = 1;		//置已访问标记
			queue.Append(w);	//顶点w入队列
			}
			//取顶点u的邻接顶点w的下一个邻接顶点
			w = GetNextNeighbor(u, w);
		}
	}
}
非连通图BFS
void AdjMWGraph::BroadFirstSearch(void Visit(VerT item))
//非连通图G访问操作为Visit()的广度优先遍历
{
	int *visited = new int[NumOfVertices()];

	for(int i = 0; i < NumOfVertices(); i++) 
visited[i] = 0;
	for(i = 0; i < NumOfVertices(); i++)
		if(!visited[i]) 
BroadFirstSearch(i, visited, Visit);

	delete []visited;
}

0x04 图的应用-最小生成树


基本概念

一个有n个顶点的连通图的生成树是原图的极小连通子图,它包含原图中的所有n个顶点,并且有保持图连通的最少的边。
注意:

(1)若在生成树中删除一条边就会使该生成树因变成非连通图而不再满足生成树的定义;
(2)若在生成树中增加一条边就会使该生成树中因存在回路而不再满足生成树的定义;
(3)一个连通图的生成树可能有许多;
(4)有n个顶点的无向连通图,无论它的生成树的形状如何,一定有且只有n-1条边。 

下面是无向图和它的几个不同的生成树。
生成树.png

如果无向连通图是一个带权图,那么它的所有生成树中必有一棵边的权值总和最小的生成树,我们称这棵生成树为最小代价生成树,简称最小生成树

典型构造方法有Prim算法和Kruskal算法(普利姆算法和克鲁斯卡尔算法)。

Prim算法

假设G=(V,E)为一个带权图,其中V为带权图中顶点的集合,E为带权图中边的权值集合。设置两个新的集合U和T,其中U用于存放带权图G的最小生成树的顶点的集合,T用于存放带权图G的最小生成树的权值的集合。普里姆算法思想是:令集合U的初值为U={u0}(即假设构造最小生成树时从顶点u0开始),集合T的初值为T={}。从所有顶点u∈U和顶点v∈V-U的带权边中选出具有最小权值的边(u,v),将顶点v加入集合U中,将边(u,v) 加入集合T中。如此不断重复,当U=V时则最小生成树构造完毕。此时集合U中存放着最小生成树顶点的集合,集合T中存放着最小生成树边的权值集合。

总结:每次挑与已有顶点相邻的有"最小"权值的边。
下面是从A开始的普利姆算法。
普利姆算法.png

具体算法略,时间复杂度O(n平方)

Kruskal算法

按照带权图中边的权值的递增顺序构造最小生成树。(如果这条边的两个顶点已经在生成树T中,则将此边舍去。
克鲁斯卡尔.png


0x05 图的应用-最短路径


路径长度:一条路径上所经过的边的数目。
带权路径长度:路径上所经过边的权值之和。
最短路径:(带权)路径长度(值)最小的那条路径。
最短路径长度最短距离:最短(带权)路径长度

迪克斯特拉算法

按路径长度递增的顺序逐步产生最短路径
狄克斯特拉算法的思想是:
设置两个顶点的集合S和T,集合S中存放已找到最短路径的顶点,集合T中存放当前还未找到最短路径的顶点。初始状态时,集合S中只包含源点,设为v0,然后从集合T中选择到源点v0路径长度最短的顶点u加入到集合S中,集合S中每加入一个新的顶点u都要修改源点v0到集合T中剩余顶点的当前最短路径长度值,集合T中各顶点的新的当前最短路径长度值,为原来的当前最短路径长度值与从源点过顶点u到达该顶点的路径长度中的较小者。此过程不断重复,直到集合T中的顶点全部加入到集合S 中为止。

迪克斯特拉.png

迪克斯特拉函数实现

函数共有4个参数,其中2个为输入参数,分别为带权图G和源点序号v0;2个为输出参数,分别为distance[]和path[],distance[]用来存放得到的从源点v0到其余各顶点的最短距离数值,path[]用来存放得到的从源点v0到其余各顶点的最短路径上到达目标顶点的前一顶点下标。

void Dijkstra(AdjMWGraph &G, int v0, int distance[], int path[])
//带权图G从下标v0顶点到其他顶点的最短距离distance
//和相应的目标顶点的前一顶点下标path
{
	int n = G.NumOfVertices();
	int *s = new int[n];			// s用来存放n个顶点的标记
	int minDis, i, j, u;
     //初始化 
	for(i = 0; i < n; i ++)								
	{
		distance[i] = G.GetWeight(v0, i);
		s[i] = 0;				    //初始均标记为0
		if(i != v0 && distance[i] < MaxWeight) 
                path[i] = v0;			   //初始的目标顶点的前一顶点均为v0
		else path[i] = -1;
	}
	s[v0] = 1;       		                 //标记顶点v0已从集合T加入到集合S中 

	//在当前还未找到最短路径的顶点集中选取具有最短距离的顶点u
	for(i = 1; i < n; i ++)
	{
		minDis = MaxWeight;
		for(j = 0; j < n; j ++)
			if(s[j] == 0 && distance[j] < minDis)
			{
				u = j;
				minDis = distance[j];
			}
                             //当已不存在路径时算法结束;此语句对非连通图是必须的
		if(minDis == MaxWeight) return;

		s[u] = 1;     		//标记顶点u已从集合T加入到集合S中

		//修改从v0到其他顶点的最短距离和最短路径
		for(j = 0; j < n; j++)
			if(s[j] == 0 && G.GetWeight(u, j) < MaxWeight && 
				distance[u] + G.GetWeight(u, j) < distance[j])
			{
			//顶点v0经顶点u到其他顶点的最短距离和最短路径
				distance[j] = distance[u] + G.GetWeight(u, j);
				path[j] = u;
			}
	}
}

每对顶点之间的最短路径

带权有向图,每对顶点之间的最短路径可通过调用狄克斯特拉算法实现。
具体方法是:每次以不同的顶点作为源点,调用狄克斯特拉算法求出从该源点到其余顶点的最短路径。重复n次就可求出每对顶点之间的最短路径。由于狄克斯特拉算法的时间复杂度为O(n的2次方),所以这种算法的时间复杂度为O(n的3次方)。

弗洛伊德算法的思想是:设矩阵cost用来存放带权有向图G的权值,即矩阵元素cost[i][j]中存放着下标为i的顶点到下标为j的顶点之间的权值,可以通过递推构造一个矩阵序列A0,A1,A2,……,AN来求每对顶点之间的最短路径。
具体算法实现略,可参考书上内容。


0x06 拓扑排序、关键路径(略)


posted @ 2020-07-02 20:02  LieDra  阅读(594)  评论(0)    收藏  举报