400 likes | 609 Views
第十九讲 最短路径. 1. 教材与参考资料. 2. 普通高等教育“十一五”国家级规划教材 普通高等教育精品教材 《 算法与数据结构 — C 语言描述 》 (第 2 版) 张乃孝 编著 , 高等教育出版社 2008. 第 9 章 图( 9.5 ) 普通高等教育“十一五”国家级规划教材配套参考书 《 算法与数据结构 》 (第 2 版)学习指导与习题解析 张乃孝 编著 , 高等教育出版社 2009. 9.5 最短路径. 如果图中从一个顶点可以到达另一个顶点,则称这两个顶点间存在一条路径。
E N D
教材与参考资料 2 • 普通高等教育“十一五”国家级规划教材 普通高等教育精品教材 《算法与数据结构— C语言描述》(第2版) 张乃孝 编著, 高等教育出版社 2008. 第9章 图(9.5) • 普通高等教育“十一五”国家级规划教材配套参考书 《算法与数据结构》(第2版)学习指导与习题解析 张乃孝 编著, 高等教育出版社 2009.
9.5 最短路径 • 如果图中从一个顶点可以到达另一个顶点,则称这两个顶点间存在一条路径。 • 从一个顶点到另一个顶点间可能存在多条路径,而每条路径上经过的边数并不一定相同。 • 如果图是一个带权图,则路径长度为路径上各边的权值的总和, • 两个顶点间路径长度最短的那条路径称为两个顶点间的最短路径,其路径长度称为最短路径长度。
9.5.1 Dijkstra算法 • Dijkstra算法求解从顶点v0出发到其它各顶点最短路径。 • 该算法假设所有边的权都大于等于零。
基本思想 • 设置一个集合U,存放已求出最短路径的顶点,V-U是尚未确定最短路径的顶点集合。 • U的初始状态为{v0}。 • 按路径长度递增的次序逐个产生vx的最短路径,把vx加入U中。直到U = V时终止。
v0 U vi2 vi3 vi1 距离值 为每个顶点设置一个距离值(已知最短距离): 集合U中某顶点的距离值就是从顶点v0到该顶点的最短路径长度; 集合V-U中某顶点的距离值是从顶点v0到该顶点的只包括集合U中顶点为中间顶点的最短路径长度。 性质:若 vi是V-U中距离值最小的结点,那么这个距离值就是从v0到vi的最短路径。 。 V - U 图 G
按路径长度递增的次序逐个产生最短路径 初始状态: • 集合U中只有顶点v0,顶点v0对应的距离值为0,集合V-U中顶点vi的距离值为边(v0, vi) (i=1,2,…,n-1)的权,如果v0和vi间无边直接相连,则vi的距离值为∞(实际程序中可以用一个足够大的数代替)。 处理框架: • (1)在集合V-U中选择距离值最小的顶点vmin加入集合U; • (2)对集合V-U中各顶点的距离值进行修正:如果加入顶点vmin为中间顶点后,使v0到vi的距离值比原来的距离值更小,则修改vi的距离值。 • (3)重复(1)(2)操作,直到从v0出发可以到达的所有顶点都在集合U中为止。
例:已知带权图G10如下图所示及其邻接矩阵A,求从顶点v0到其它各顶点的最短路径例:已知带权图G10如下图所示及其邻接矩阵A,求从顶点v0到其它各顶点的最短路径 45 v0 v4 50 v1 5 ¥ ¥ é ù 0 50 10 45 ê ú 15 35 ¥ ¥ ¥ 10 0 15 5 ê ú 20 20 30 ê ú ¥ ¥ ¥ 20 0 15 = v2 A ê ú v3 v5 ¥ ¥ ¥ 15 3 20 0 35 ê ú ê ú ¥ ¥ ¥ ¥ 30 0 45 [45] v0 v4 ê ú [50] 50 v1 5 ¥ ¥ ¥ ¥ ê ú 3 0 ë û 15 35 10 20 20 30 [¥] v2 [10] [¥] v3 v5 15 3 绿色为从U到V-U的边, 选 (v0, v2) 把v2加入U
[45] [45] 45 45 v0 v4 v0 v4 [50] 50 v1 5 50 v1 5 15 35 15 35 10 10 20 20 20 20 30 30 [¥] [25] [¥] v2 v2 v3 v5 v3 v5 15 3 15 3 [45] 45 45 v0 v4 v0 v4 [45] 50 v1 5 50 v1 5 15 35 15 35 10 10 20 20 20 20 30 30 [¥] v2 v2 v3 v5 v3 v5 15 3 15 3 选 (v2, v3),把v3加入U 选 (v0, v4),把v4加入U 选 (v3, v1),把v1加入U
存储结构 • 图选择邻接矩阵表示法,其中关系矩阵的对角线初值均取0。算法中,将放入集合U中结点对应的关系矩阵中对角线元素值修改为1; • 另外设置一个数组dist,dist[i]用于存放v0到顶点vi的最短路径及其最短路径长度(计算过程中为距离值)∶ ypedef struct { AdjType length; /* 最短路径长度 */ int prevex; /*从v0到达vi(i=1,2,…n-1)的最短路 径上vi的前趋顶点*/ }Path; Path* dist;
修正距离值方法的精化 在加入顶点vmin为中间顶点后,如果 dist[i].length>dist[min].length+G.arcs[min][i], 则将顶点vi的距离值改为: dist[min].length+G.arcs[min][i] 并修改路径上vi的前趋顶点: dist[i].prevex=min。
在数组dist上模拟算法 • ①.初始时,集合U中只有顶点v0。 • 结果dist[n]为{{0, 0}, {50, 0}, {10, 0}, {MAX, -1}, {45, 0}, {MAX, -1} } • ②.在集合V-U中找出距离值最小的顶点v2,将顶点v2加入集合U中。 • 结果dist[n]为{{0, 0}, {50, 0}, {10, 0}, {MAX, -1}, {45, 0}, {MAX, -1} } • ③.按min=2调整集合V-U中顶点的距离值。 • 因为dist[1].legth=50,dist[2].length+graph.arcs[2][1]=10+MAX,因此,顶点v1的距离值不需要调整。 • 因为dist[3].length=MAX,dist[2].length+graph.arcs[2][3]=10+15=25,因此,顶点v3的距离值调整为25,其前趋顶点为v2。 • 同理,顶点v4 ,v5的距离值不需要调整。 • 结果dist[n]为{{0, 0}, {50, 0}, {10, 0}, {25, 2}, {45, 0}, {MAX, -1} }
在数组dist上模拟算法(续) • ④.同理在集合V-U中找出当前距离值最小的顶点v3,并调整集合V-U中顶点的距离值。 • 结果dist[n]为{{0, 0}, {45, 3}, {10, 0}, {25, 2}, {45, 0}, {MAX, -1} } • ⑤.在集合V-U中找出当前距离值最小的顶点v1。并调整集合V-U中顶点的距离值。 • 结果dist[n]为{{0, 0},{45, 3}, {10, 0}, {25, 2}, {45, 0}, {MAX, -1} } • ⑥.在集合V-U中找出当前距离值最小的顶点v4。并调整集合V-U中顶点的距离值。 • 结果dist[n]为{{0, 0}, {45, 3}, {10, 0}, {25, 2}, {45, 0}, {MAX, -1} } • ⑦.没有可以再加入集合U的顶点了,说明从顶点v0到顶点v5之间无路径相通。过程结束。
由数组dist的prevex字段得到顶点v0到各顶点的最短路径由数组dist的prevex字段得到顶点v0到各顶点的最短路径 {{0, 0}, {45, 3}, {10, 0}, {25, 2}, {45, 0}, {MAX, -1} } • 如从v0到v1的最短路径,dist[1].prevex=3可知路径上顶点v1的前一个顶点是v3, • dist[3].prevex=2可知路径上顶点v3的前一个顶点是v2, • dist[2].prevex=0可知路径上前一个顶点是v0,即最短路径为(v0 , v2 , v3 , v1)。
初始化dist[] void init(GraphMatrix* pgraph, Path dist[]) { int i; dist[0].length = 0; dist[0].prevex = 0; pgraph->arcs[0][0] = 1; /* 用矩阵对角线元素记录集合 U,为 1 表示在U中。*/ for(i = 1; i < pgraph->n; ++ i) { /* 初始化V-U中顶点的距离 值*/ dist[i]. length = pgraph->arcs[0][i]; if (dist[i]. length != MAX) dist[i].prevex = 0; else dist[i].prevex = -1; } }
Dijkstra算法 void dijkstra(GraphMatrix *graph, Path dist[]) { int i, j, mv, n = graph->n; AdjType minw; init(graph, dist); /* 初始化,集合U中只有顶点v0 */ for(i = 1; i < n; ++ i) { minw = MAX; mv = 0; for (j = 1; j < n; ++ j) /*在V-U中选出距 v0 最近的顶点*/ if( graph->arcs[j][j] == 0 && dist[j].length < minw ) { mv = j; minw = dist[j].len; } if (mv == 0) break; /* v0 与 V-U 的顶点不连通,结束 */ graph->arcs[mv][mv] = 1;/* 顶点mv加入U */ for (j = 1; j < n; ++ j) { /*调整V-U 顶点的已知最短路径*/ if (graph->arcs[j][j] == 0 && /*为V-U的顶点*/ dist[j]. length > dist[mv].length+ graph->arcs[mv][j]) { dist[j].prevex = mv; /* 调整已知最短路径信息 */ dist[j]. length = dist[mv]. length + graph->arcs[mv][j]; } } } }
算法分析 算法中的初始化部分的时间复杂度为O(n), 求最短路径部分由一个大循环组成,其中外循环运行n-1次,内循环为两个,均运行n-1次,因此,算法的时间复杂度为O(n2)。 另外需要注意,在算法中改变了关系矩阵中的初始状态,如果要求算法不能破坏原始数据,就需要另外增加数据结构记录U集合的值。空间代价是O(n).
9.5.2 Floyd算法 功能:求带权图每一对顶点间的最短路径 基本思想: • 图采用邻接矩阵作为存储结构。把关系矩阵看成是没有经过任何中间结点,直接可以到达的每一对顶点间的最短路径。 • 然后按结点在结点表中的顺序,每次考虑增加一个新的结点,在允许这个结点作为中间结点的条件下,计算每一对顶点间的最短路径的缩短变化。 • 直到把所有结点都考虑进去为止,结果得到每一对顶点间的最短路径。
数据结构 为了在算法中不破坏原始的关系矩阵,需要定义一个与关系矩阵同样大小,以关系矩阵作为其初始状态的矩阵,用于存放处理中每对顶点间的距离值(或最短路径长度)。 为了保存全部最短路径的轨迹,需要另外设计一个与关系矩阵同样大小的整数矩阵,存放vi到vj最短路径上vi的后继顶点的下标值。把它们封装在下面定义的结构类型ShortPath 中: typedef struct{ AdjType *a[]; /*存放每对顶点间最短路径长度 */ int *nextvex[]; /*存放vi到vj最短路径上vi的后继顶点的下标值 */ }ShortPath;
Floyd算法 void Floyd(GraphMatrix * pgraph, ShortPath * ppath) { int i, j, k, n = pgraph->n; for (i = 0; i < n; ++ i) for (j = 0; j < n; ++ j) { if (pgraph->arcs[i][j] != MAX) ppath->nextvex[i][j] = j; else ppath->nextvex[i][j] = -1; ppath->a[i][j] = pgraph->arcs[i][j]; /* 复制邻接矩阵 */ } for (k = 0; k < n; ++ k) for (i = 0; i < n; ++ i) for (j = 0; j < n; ++ j) { if ( ppath->a[i][k] == MAX || ppath->a[k][j] == MAX ) continue; if ( ppath->a[i][j] > ppath->a[i][k]+ ppath->a[k][j] ) { ppath->a[i][j] = ppath->a[i][k] + ppath->a[k][j]; ppath->nextvex[i][j] = ppath->nextvex[i][k]; } } }
代价分析 算法中初始化部分由一个两重循环组成,其中外循环运行n次,内循环也运行n次,初始化部分的时间复杂度为O(n2)。 迭代生成最短路径长度矩阵A和路径矩阵nextvex的部分为三重循环的嵌套,其时间复杂度为O(n3)。 Floyd算法的时间复杂度为O(n3)。 空间代价是O(n2) (比较大)。
例题 • 用Floyd方法求图G10各顶点间的最短路径长度。
路径的查找 例如,想知道v0到v1的最短路径: • 由A[0][1]可知v0到v1的最短路径长度为45, • 路径由nextvex[0][1]=2可知顶点v0的下一顶点为v2, • 由nextvex[2][1]=3可知v2的下一顶点为v3, • 由nextvex[3][1]=1可知v3的下一顶点为v1, • 因此从v0到v1的最短路径为v0→v2→v3→v1
本讲重点: • 图中从一个顶点到另一个顶点间路径长度最短的路径称为两个顶点间的最短路径。本章介绍了从顶点v0出发到其它顶点最短路径的Dijkstra算法和求每一对顶点间的最短路径的Floyd算法。
讨论:动态规划算法* • 有些问题在分解时会产生大量的子问题,同时分得的子问题界限不清,互相交叉,因而在解这类问题时,将可能重复多次解同一个子问题。 • 解决的方法可以在解决每个子问题后把它的解(包括其子子问题的解)保留在一个表格中,若遇到求与之相同的子问题的解时,就可以从表中把解找出来直接使用。 • 本书所见过的运用动态规划法的算法有:所有结点间的最短路径算法(算法9.7)及最佳二叉排序树的构造算法(算法7.5)。
回顾:不等概率的最佳二叉排序树的构造 给定排序的关键码集合 {key1, key2,…,keyn} 和两个权集合{p1,…, pn}和{ q0, q1 …,qn}, 其中pi是检索内部结点i的概率,qj是被检索关键码属于外部结点j 的关键码集合的概率. 问题: 要求构造一棵最佳二叉排序树, 即构造出一棵二叉排序树,使下面的函数达到最小(E(n)一定也达到最小): 上式称为包含 n 个内部结点和 n+1个外部结点的二叉排序树的花费, 记作 C(0, n). 性质:最佳二叉排序树里的任何子树都最佳.
花费的计算 • 在构造 T(i, j) 时,对所有 i < k≤ j,T(i, k-1) 和 T(k, j) 都已存在, 而且已知相应花费 C(i, k) 和 C(k, j). • 对每个 k, 以keyk为根,T(i, k-1) 和 T(k, j) 为左右子树的二叉树的花费为 : Ck(i, j) = W(i, j) + C(i, k-1)+C(k, j) (结点增加一层). • 从所有可能 k 中选择使 Ck(i, j) 达到最小的那个 k,与之对应树就是 T(i, j),其根 R(i, j) = k. • 新树的代价可以由构造它的已知最佳二叉排序树的代价算出: C(i,j)=W(i,j)+min ikj(C(i, k-1)+C(k,j)). • NOTE: T(i,i)为不包含任何内部结点, 仅仅由权值为qi的外部结点构成, C(i,i)=0.
数据的存储 数组p存放内部结点的权(p[1],…,p[n]); 数组q存放外部结点的权(q[0],…,q[n]); c[i][j] 保存T(i,j)的花费; w[i][j] 保存T(i,j)的权; r[i][j] 保存T(i,j)的根. 构造好的最佳二叉排序树的总花费存放在 c[0][n]; r[0][n] 是根结点的编号。根据 r[0][n] 的值,就可以确定两棵子二叉树的根在那里:
直接算出 w : 4 12 14 17 0 3 5 8 0 0 1 4 0 0 0 1 第一步 第二步 第三步 r : 0 1 0 0 0 0 2 0 0 0 0 3 0 0 0 0 r : 0 1 1 0 0 0 2 2 0 0 0 3 0 0 0 0 r : 0 1 1 1 0 0 2 2 0 0 0 3 0 0 0 0 c : 0 12 0 0 0 0 5 0 0 0 0 4 0 0 0 0 c : 0 12 19 0 0 0 5 12 0 0 0 4 0 0 0 0 c : 0 12 19 29 0 0 5 12 0 0 0 4 0 0 0 0 计算结果: P : {5,1,2} Q: {4,3,1,1} C(i,j)=W(i,j)+min ikj(C(i, k-1)+C(k,j)).
贪心法 1,求着色问题近似解的贪心法。 2,构造最小生成树的 prim 算法(算法9.5)。这里的条件是所有权都是正数,在遇到带有负权边的图时,这样做就可能产生错误的结果。 3,Dijkstra 的最短路径算法(算法9.6)求从源点到其他各结点的最短路径。这里的条件是所有权都是正数,在遇到带有负权边的图时,这样做就可能产生错误的结果。
动态规划法与贪心法的相同点 两者的相同点是都作出一系列的选择和确定: • 动态规划法是将子问题的解确定后,利用它们经过一系列的选择和确定逐步导出原问题的解。 • 而贪心算法是从局部到整体进行了一系列的选择和确定,确定了一个较好的解。
动态规划法与贪心法的区别 它们的一个区别是: • 动态规划法先求子问题的解,然后构造原问题的解; • 而贪心算法是直接地解原问题。 另一更大的区别是: • 动态规划法一般产生多个子问题的解,每个子问题的解都是局部最优的。然后通过对这些局部解的组合进行比较,去掉了次优解,产生一个上一层次的(局部)最优解。 • 贪心法每阶段只作一个挑选,这个挑选在各阶段是最优的,但是一经选出就固定不变了,后阶段的最优是在前阶段的挑选基础上。所以最后获得的往往只能是次优解。 Kruskal 的求最小生成树算法(见9.4 节)?
分治法 把一个规模为n 的问题分成两个或多个较小的与原问题类型相同的子问题,通过对子问题的求解,并把子问题的解合并起来从而构造出整个问题的解,即对问题分而治之。 • 二分法检索(算法6.12)。 • 快速排序算法(算法8.8)。 • 归并排序算法(算法8.12) 。
分治法的算法框架 return_type d_and_c(objArray *p,int i,int j) { int temp ; if (simple (p,i,j)) return solve(p,i,j) ; temp =divide (p,i,j) ; return (combine (d_and_c(p,i,temp -1), d_and_c(p,temp,j))); }
动态规划法与分治法比较 动态规划法与分治法的共同点是:把一个大问题分解为若干较小的子问题,通过求解子问题而得到原问题的解。 不同点是: • 分治法每次分解的子问题数目比较少,子问题之间界限清楚,处理的过程通常是自顶向下进行; • 动态规划法分解的子问题可能比较多,而且子问题相互包含,为了重用已经计算的结果,要把计算的中间结果全部保存起来,通常是自底向上进行。