短路径问题是图的又一个比较典型的应用问题。例如,n 个城市之间的一 个公路网,给定这些城市之间的公路的距离,能否找到城市 A 到城市 B 之间一 条距离近的通路呢?如果城市用顶点表示,城市间的公路用边表示,公路的长 度作为边的权值。那么,这个问题就可归结为在网中求顶点 A 到顶点 B 的所有 路径中边的权值之和小的那一条路径,这条路径就是两个顶点之间的短路径 (Shortest Path),并称路径上的第一个顶点为源点(Source),后一个顶点为终 点(Destination)。在不带权的图中,短路径是指两个顶点之间经历的边数 少的路径。
短路径可以是求某个源点出发到其它顶点的短路径,也可以是求网中任 意两个顶点之间的短路径。这里只讨论单源点的短路径问题,感兴趣的读者 可参考有关文献,了解每一对顶点之间的短路径
网分为无向网和有向网,当把无向网中的每一条边(vi,vj)都定义为弧
和弧 ,则有向网就变成了无向网。因此,不失一般性,我们这里只讨论有 向网上的短路径问题。 图 6.17 是一个有向网及其邻接矩阵。该网从顶点 A 到顶点 D 有 4 条路径, 分别是:路径(A,D),其带权路径长度为 30;路径(A,C,F,D),其带权路径长 度为 22;路径(A,C,B,E,D),其带权路径长度为 32;路径(A,C,F,E,D),其带权 路径长度为 34。路径(A,C,F,D)称为短路径,其带权路径长度 22 称为短距离。
对于求单源点的短路径问题,狄克斯特拉(Dikastra)提出了一个按路径 长度递增的顺序逐步产生短路径的构造算法。狄克斯特拉的算法思想是:设置 两个顶点的集合 S 和 T,集合 S 中存放已找到短路径的顶点,集合 T 中存放当 前还未找到短路径的顶点。初始状态时,集合 S 中只包含源点,设为 v0,然 后从集合 T 中选择到源点 v0 路径长度短的顶点 u 加入到集合 S 中,集合 S 中 每加入一个新的顶点 u 都要修改源点 v0 到集合 T 中剩余顶点的当前短路径长 度值,集合 T 中各顶点的新的短路径长度值为原来的当前短路径长度值与 从源点过顶点 u 到达该顶点的新的短路径长度中的较小者。此过程不断重复, 直到集合 T 中的顶点全部加到集合 S 中为止。
【例 6-5】以图 6.17 为例说明用狄克斯特拉算法求有向网的从某个顶点到其 余顶点短路径的过程
图6.18(a)~(f)给出了狄克斯特拉算法求从顶点A到其余顶点的短路径的过 程。图中虚线表示当前可选择的边,实线表示算法已确定包括到集合 S 中所有顶 点所对应的边。
第一步:列出顶点 A 到其余各顶点的路径长度,它们分别为 0、∞、5、30、 ∞、∞。从中选取路径长度小的顶点 C(从源点到顶点 C 的短路径为 5)。
第二步:找到顶点 C 后,再观察从源点经顶点 C 到各个顶点的路径是否比 第一步所找到的路径要小(已选取的顶点则不必考虑),可发现,源点到顶点 B 的路径长度更新为 20(A,C,B),源点到顶点 F 的路径长度更新为 12(A,C, F),其余的路径则不变。然后,从已更新的路径中找出路径长度小的顶点 F(从 源点到顶点 F 的短路径为 12)。
第三步:找到顶点 C、F 以后,再观察从源点经顶点 C、F 到各顶点的路径 是否比第二步所找到的路径要小(已被选取的顶点不必考虑),可发现,源点到 顶点 D 的路径长度更新为 22(A,C,F,D),源点到顶点 E 的路径长度更新为 30(A,C,F,E),其余的路径不变。然后,从已更新的路径中找出路径长短 小的顶点 D(从源点到顶点 D 的短路径为 22)。
第四步:找到顶点 C、F、D 后,现在只剩下后一个顶点 E 没有找到短 路径了,再观察从源点经顶点 C、F、D 到顶点 E 的路径是否比第三步所找到的路径要小(已选取的顶点则不必考虑),可以发现,源点到顶点 E 的路径长度更 新为 28(A,B,E),其余的路径则不变。然后,从已更新的路径中找出路径长 度小的顶点 E(从源点到顶点 E 的短路径为 28)。
本文以有向网的邻接矩阵类 DirecNetAdjMatrix
来实现狄克斯特拉算法。 DirecNetAdjMatrix 有三个成员字段,一个是 Node 类型的一维数组 nodes, 存放有向网中的顶点信息,一个是整型的二维数组 matirx,表示有向网的邻接矩 阵,存放弧的信息,一个是整数 numArcs,表示有向网中弧的数目,有向网的邻 接矩阵类 DirecNetAdjMatrix 的实现如下所示。
public class DirecNetAdjMatrix : IGraph
{
private Node[] nodes; //有向网的顶点数组
private int numArcs; //弧的数目
private int[,] matrix; //邻接矩阵数组
//构造器
public DirecNetAdjMatrix(int n)
{
nodes = new Node[n];
matrix = new int[n, n];
numArcs = 0;
}
//获取索引为index的顶点的信息
public Node GetNode(int index)
{
return nodes[index];
}
//设置索引为index的顶点的信息
public void SetNode(int index, Node v)
{
nodes[index] = v;
}
//弧数目属性
public int NumArcs
{
get
{
return numArcs;
}
set
{
numArcs = value;
}
}
//获取matrix[index1, index2]的值
public int GetMatrix(int index1, int index2)
{
return matrix[index1, index2];
}
//设置matrix[index1, index2]的值
public void SetMatrix(int index1, int index2, int v)
{
matrix[index1, index2] = v;
}
//获取顶点数目
public int GetNumOfVertex()
{
return nodes.Length;
}
//获取弧的数目
public int GetNumOfEdge()
{
return numArcs;
}
//判断v是否是网的顶点
public bool IsNode(Node v)
{
//遍历顶点数组
foreach (Node nd in nodes)
{
//如果顶点nd与v相等,则v是图的顶点,返回true
if (v.Equals(nd))
{
return true;
}
}
return false;
}
//获取v在顶点数组中的索引
public int GetIndex(Node v)
{
int i = -1;
//遍历顶点数组
for (i = 0; i < nodes.Length; ++i)
{
//如果顶点nd与v相等,则v是图的顶点,返回索引值
if (nodes[i].Equals(v))
{
return i;
}
}
return i;
}
//在v1和v2之间添加权为v的弧
public void SetEdge(Node v1, Node v2, int v)
{
//v1或v2不是网的顶点
if (!IsNode(v1) || !IsNode(v2))
{
Debug.WriteLine("Node is not belong to Graph!"); return;
}
matrix[GetIndex(v1), GetIndex(v2)] = v;
++numArcs;
}
//删除v1和v2之间的弧
public void DelEdge(Node v1, Node v2)
{
//v1或v2不是网的顶点
if (!IsNode(v1) || !IsNode(v2))
{
Debug.WriteLine("Node is not belong to Graph!"); return;
}
//v1和v2之间存在弧
if (matrix[GetIndex(v1), GetIndex(v2)] != int.MaxValue)
{
matrix[GetIndex(v1), GetIndex(v2)] = int.MaxValue;
--numArcs;
}
}
//判断v1和v2之间是否存在弧
public bool IsEdge(Node v1, Node v2)
{
//v1或v2不是网的顶点
if (!IsNode(v1) || !IsNode(v2))
{
Debug.WriteLine("Node is not belong to Graph!"); return false;
}
//v1和v2之间存在弧
if (matrix[GetIndex(v1), GetIndex(v2)] != int.MaxValue)
{
return true;
}
else
{
return false;
}
}
}
为实现狄克斯特拉算法,引入两个数组,一个一维数组 ShortPathArr,用来保存从源点到各个顶点的 短路径的长度,一个二维数组 PathMatrixArr,用来保存从源点到某个顶点的 短路径上的顶点,如 PathMatrix[v][w]为 true,则 w 为从源点到顶点 v 的 短路径上的顶点。为了该算法的结果被其他算法使用,把这两个数组作为算法的参数使用。另外,为了表示某顶点的 短路径是否已经找到,在算法中设了一个一维数组 final,如果 final[i]为 true,则表示已经找到第 i 个顶点的 短路径。i 是该顶点在邻接矩阵中的序号。同样,把该算法作为类
DirecNetAdjMatrix
的成员方法来实现。
public void Dijkstra(ref bool[,] pathMatricArr, ref int[] shortPathArr, Node n)
{
int k = 0; bool[] final = new bool[nodes.Length];
//初始化
for (int i = 0; i < nodes.Length; ++i)
{
final[i] = false; shortPathArr[i] = matrix[GetIndex(n), i];
for (int j = 0; j < nodes.Length; ++j)
{
pathMatricArr[i, j] = false;
}
if (shortPathArr[i] != 0 && shortPathArr[i] < int.MaxValue)
{
pathMatricArr[i, GetIndex(n)] = true;
pathMatricArr[i, i] = true;
}
}
// n为源点
shortPathArr[GetIndex(n)] = 0; final[GetIndex(n)] = true;
//处理从源点到其余顶点的 短路径
for (int i = 0; i < nodes.Length; ++i)
{
int min = int.MaxValue;
//比较从源点到其余顶点的路径长度
for (int j = 0; j < nodes.Length; ++j)
{
//从源点到j顶点的 短路径还没有找到
if (!final[j])
{
// 从源点到j顶点的路径长度 小
if (shortPathArr[j] < min)
{
k = j; min = shortPathArr[j];
}
}
}
//源点到顶点k的路径长度 小
final[k] = true;
//更新当前 短路径及距离
for (int j = 0; j < nodes.Length; ++j)
{
if (!final[j] && (min + matrix[k, j] < shortPathArr[j]))
{
shortPathArr[j] = min + matrix[k, j]; for (int w = 0; w < nodes.Length; ++w)
{
pathMatricArr[j, w] = pathMatricArr[k, w];
}
pathMatricArr[j, j] = true;
}
}
}
}
拓扑排序(Topological Sort)是图中重要的运算之一,在实际中应用很广泛。例如,很多工程都可分为若干个具有独立性的子工程,我们把这些子工程称为“活动”。每个活动之间有时存在一定的先决条件关系,即在时间上有着一定的相互制约的关系。也就是说,有些活动必须在其它活动完成之后才能开始,即某项活动的开始必须以另一项活动的完成为前提。在有向图中,若以图中的顶点表示活动,以弧表示活动之间的优先关系,这样的有向图称为 AOV 网(Active On Vertex Network)。
在 AOV 网中,若从顶点 vi 到顶点 vj 之间存在一条有向路径,则称 vi 是 vj 的前驱,vj 是 vi 的后继。若
是 AOV 网中的弧,则称 vi 是 vj 的直接前驱, vj 是 vi 的直接后继。
例如,一个软件专业的学生必须学习一系列的基本课程(如表 6.2 所示)。其中,有些课程是基础课,如“高等数学”、“程序设计基础”,这些课程不需要先修课程,而另一些课程必须在先学完某些课程之后才能开始学习。如通常在学完“程序设计基础”和“离散数学”之后才开始学习“数据结构”等等。因此,
可以用 AOV 网来表示各课程及其之间的关系,如图 6.19 所示。
表 6.2 软件专业必修课程
课程编号 |
课程名称 |
先决条件 |
c1 |
程序设计基础 |
无 |
c2 |
离散数学 |
c1 |
c3 |
数据结构 |
c1,c2 |
c4 |
汇编语言 |
c1 |
c5 |
语言的设计与实现 |
c3,c4 |
c6 |
计算机原理 |
c11 |
c7 |
编译原理 |
c3,c5 |
c8 |
操作系统 |
c3,c6 |
c9 |
高等数学 |
无 |
c10 |
线性代数 |
c9 |
c11 |
普通物理 |
c9 |
c12 |
数值分析 |
c9,c10,c11 |
在 AOV 网中,不应该出现有向环路,因为有环意味着某项活动以自己作为先决条件,这样就进入了死循环。如果图 6.19 的有向图出现了有向环路,则教学计划将无法编排。因此,对给定的 AOV 网应首先判定网中是否存在环。检测的办法是对有向图进行拓扑排序(Topological Sort),若网中所有顶点都在它的拓扑有序序列中,则 AOV 网中必定不存在环。
实现一个有向图的拓扑有序序列的过程称为拓扑排序。可以证明,任何一个有向无环图,其全部顶点都可以排成一个拓扑序列,而其拓扑有序序列不一定是唯一的。例如,图 6.19 的有向图的拓扑有序序列有多个,这里列举两个如下:
(c1,c2,c3,c4,c5,c7,c8,c9,c10,c11,c6,c12,c8) 和 (c9,c10,c11,c6,c1,c12,c4,c2,c3,c5,c7,c8)
由上面两个序列可知,对于图中没有弧相连的两个顶点,它们在拓扑排序的序列中出现的次序没有要求。例如,第一个序列中 c1 先于 c9,第二个则反之。拓扑排序的任何一个序列都是一个可行的活动执行顺序,它可以检测到图中是否存在环,因为如果有环,则环中的顶点无法输出,所以得到的拓扑有序序列没有包含图中所有的顶点。
下面是拓扑排序算法的描述:
(1)在有向图中选择一个入度为 0 的顶点(即没有前驱的顶点),由于该顶 点没有任何先决条件,输出该顶点;
(2)从图中删除所有以它为尾的弧;
(3)重复执行(1)和(2),直到找不到入度为 0 的顶点,拓扑排序完成。
如果图中仍有顶点存在,却没有入度为 0 的顶点,说明 AOV 网中有环路,否则没有环路。
【例 6-6】以图 6.20(a)为例求出它的一个拓扑有序序列。
第一步:在图 6.20(a)所示的有向图中选取入度为 0 的顶点 c4,删除顶点 c4 及与它相关联的弧
, ,得到图 6.31(b)所示的结果,并得到第一个 拓扑有序序列顶点 c4。 第二步:再在图 6.20(b)中选取入度为 0 的顶点 c5,删除顶点 c5 及与它相关 联的弧
,得到图 6.20(c)所示的结果,并得到两个拓扑有序序列顶点 c4, c5。 第三步:再在图 6.20(c)中选取入度为 0 的顶点 c1,删除顶点 c1 及与它相关 联的弧
, ,得到图 6.20(d)所示的结果,并得到三个拓扑有序序列 顶点 c4,c5,c1。 第四步:再在图 6.20(d)中选取入度为 0 的顶点 c2,删除顶点 c2 及与它相关 联的弧
,得到图 6.20(e)所示的结果,并得到四个拓扑有序序列顶点 c4, c5,c1,c2。 第五步:再在图 6.20(e)中选取入度为 0 的顶点 c3,删除顶点 c3 及与它相关 联的弧
,得到图 6.20(f)所示的结果,并得到五个拓扑有序序列顶点 c4, c5,c1,c2,c3。 第六步:后选取仅剩下的后一个顶点 c6,拓扑排序结束,得到图 6.20(a)的一个拓扑有序序列(c4,c5,c1,c2,c3,c6)。
小结
图是另一种比树形结构更复杂的非线性数据结构,图中的数据元素称为顶 点,顶点之间是多对多的关系。图分为有向图和无向图,带权值的图称为网。
图的存储结构很多,一般采用数组存储图中顶点的信息,邻接矩阵采用矩阵 也就是二维数组存储顶点之间的关系。无向图的邻接矩阵是对称的,所以在存储 时可以只存储上三角矩阵或下三角矩阵的数据;有向图的邻接矩阵不是对称的。 邻接表用一个链表来存储顶点之间的关系,所以邻接表是顺序存储与链式存储相 结合的存储结构。
图的遍历方法有两种:深度优先遍历和广度优先遍历。图的深度优先遍历类 似于树的先序遍历,是树的先序遍历的推广,它访问顶点的顺序是后进先出,与 栈一样。图的广度优先遍历类似于树的层序遍历,它访问顶点的顺序是先进先出, 与队列一样。
图的应用很广,本章重点介绍了三个方面的应用。小生成树是一个无向连 通网中边的权值总和小的生成树。构造小生成树必须包括 n 个顶点、n-1 条 边及不存在回路。构造小生成树的常用算法有普里姆算法和克鲁斯卡尔算法两 种。
最短路径问题是图的一个比较典型的应用问题。短路径是网中求一个顶点 到另一个顶点的所有路径中边的权值之和小的路径。可以求从一个顶点到网中 其余顶点的短路径,这称之为单源点问题,也可以求网中任意两个顶点之间的 短路径。本章只讨论了单源点问题。解决单源点问题的算法是狄克斯特拉算法。
拓扑排序是图中重要的运算之一,在实际中应用很广泛。AOV 网是顶点之 间存在优先关系的有向图。拓扑排序是解决 AOV 网中是否存在环路的有效手段, 若拓扑有序序列中包含 AOV 网中所有的顶点,则 AOV 网中不存在环路,否则 存在环路
以上内容 摘自数据结构C#语言描述 此书市面上买不到了只有电子版. 留在博客上以供参考