前k短的无环路径与次短树

我很懒的  @ 2007-11-13 18:27

    把最短路径问题泛化一下,我们有时想要在一张图中求出从起点到终点的前k短的路径(最短、第2短、第3短……第k短),并且需要这些路径都是 无环的。 2006年横滨赛区的最后一题就涉及到了这个问题。

    在 这个网址有很多讨论这个问题的文章,我看了其中两篇。一篇是《 The K shortest paths problem》,把求前k短路径的问题阐述得很详细,并且介绍了一些经典的算法。但是这篇文章主要讲的是怎么求前k短 可以带环的路径。另一篇是《 The K shortest loopless paths problem》,它就在前一篇的基础上讲了怎么求前k短的 无环路径。虽然它讲得不是很详细,但是有了前一篇文章的基础,还是看得懂的。

    根据上面两篇文章所讲的,求前k短 无环路径的最好的算法是 Yen算法(一个名叫Yen的人发名的。但是Yen本人写的文章我下载不到,好像是要收费的。)。作者在文章中也介绍了他自已发明的算法,名叫MPS算法。但是根据我尝试下来,这只是实验性的算法,并不适用于所有情况(横滨赛区有一些很阴险的数据,可以让这个算法超时),所以先不去管它。

    光有这两篇文章作为理论支撑并不足以做出2006年横滨的最后一题,因为在实现上还有很多细节和优化方法需要推敲。所以我决定要写一下。

    首先我简单地描述一下Yen算法。设P i为从起点s到终点t的第i短的无环路径。一开始,P 1,也就是从s到t的最短路径,可以通过Dijkstra、Bellman-Ford或BFS等算法轻易地求出。接下来要依次求出P 2,P 3……P k

    我们把P 1~P i看成一棵树,称为T i,它的根节点是s,所有叶节点都是t。举个例子,假设s = 1,t = 5,我们求出的P 1~P 5如下:

1 -> 2 -> 3 -> 5
1 -> 3 -> 2 -> 5
1 -> 3 -> 5
1 -> 4 -> 5
1 -> 4 -> 3 -> 5

    则T 5就是图1所示的一棵树。



图 1

    设dev i为P i偏离点(deviation node),表示在T i上,P i对应的那一分枝中,第1个(按从s到t的顺序)不在T i - 1上的点(i > 1)。为了说话方便,设dev 1为P 1的第2个点。因此,在图1中,dev 1~dev 5的编号分别为2、3、5、4和3。显而易见,dev i至少是P i的第2个点。

    接下来是Yen算法的主要部分。每当求出一个P i时,都可以从P i上发展出若干条 候选路径。发展的方法是这样的,对于P i上从dev i前一个点到t的 前一个点这一段上的每个点v,都可以发展出一条候选路径。用P isv表示P i上从s到v的子路径,用P ivt表示从v到t的满足下列表条件的 最短路径:

条件1. 设点v在Ti上对应的点为v',则Pivt上从点v出发的那条边不能与Ti上从点v'出发的任何一条边相同。
条件2Pivt上,除了点v,其它点都不能出现在Pisv上。

    如果找得出P ivt,则把P isv和P ivt连起来就组成了一条候选路径。条件1保证了候选路径不与P 1~P i重复;条件2保证了候选路径无环。 

    在图1中的例子的基础上,我们举一个发展候选路径的例子。在求出了P 5之后,我们要在P 5上发展候选路径。P 5的偏离点是3号点。因此v的范围是{4, 3}。
    当v = 4时,P isv = 1 -> 4,因此,根据条件1,在P ivt上不能出现1号点。找到P 5上的4号点在T 5上对应的那一点,也就是图2中位于蓝色的3号点上面的4号点,在T 5上从它出发的有(4, 5)和(4, 3)这两条边,因此,根据条件2,在P ivt上不能出现这两条边。假设在这样的情况下,我们求出了从4号点到t的最短路径为4 -> 2 -> 5,它就是P ivt。此时发展出的候选路径就是1 -> 4 -> 2 -> 5。
    当v = 3时,P isv = 1 -> 4 -> 3,因此,根据条件1,在P ivt上不能出现1号点和4号点。找到P 5上的3号点在T 5上对应的那一点,也就是图2中蓝色的3号点,在T 5上从它出发的只有(3, 5)这一条边,因此,根据条件2,在P ivt上不能出现边(3, 5)。假设在这样的情况下,我们求出了从3号点到t的最短路径为3 -> 2 -> 5,它就是P ivt。此时发展出的候选路径就是1 -> 4 -> 3 -> 2 -> 5。



图 2

    显而易见,在从P i发展出的所有候选路径中,只有当v是dev i的前一个点时,条件1才有可能阻挡掉2条或2条以上边。当v不是dev i的前一个点时,条件1只会阻挡掉1条边,那就是本身位于P i上,从v出发的那条边。

    不仅从P i,从之前的P 1~P i - 1,我们都发展过若干条候选路径。从候选路径的集合中取出最短的一条,就是P i + 1。把P i + 1从候选路径的集合里删掉;然后再从它发展新的候选路径,添加到候选路径的集合里,如此循环,直到求出P k为止。如果在求到P k之前,候选路径的集合就空了,那么说明P k不存在。

    下面分析一下这个算法的时间复杂度。设图中有N个点,那么每条P i上最多也就有N个点,那么从P i最多发展出N条候选路径。每发展一条候选路径,当中都包含一次求最短路径的过程,它的时间大约是O(N 2)。因此从P i发展候选路径的时间最多要O(N 3)。我们要求k个P i,因此总的时间最差情况下是 O(k * N3)

    理论部分描述完了,下面就要讲一下这个算法在2006年横滨的最后一题中的具体实现和优化。这道题目中有如下约束条件:有向图,图中最多50个点,所有边长都是正数,两点之间最多只有一条边;如果有若干条长度相同的路径,则按照字典顺排名;k不超过200。下面讲一下我的做法,我用的语言是C++。

     一、数据结构

    一条路径我用vector<int>来存放,候选路径的集合我用priority_queue存放。图用矩阵表示就足够快了,因为这道题中出了很多极限数据,也就是两两点之间都有边,这时邻接链表的优势就没有了。

     二、实现条件1

    每当一条候选路径P c被发展出来时,它的偏离点dev c我们就知道了;此时我们还知道了当从P c发展新的候选路径时,如果v是dev c的前一个点,则条件1需要阻挡哪些边:就是从P i发展P c的时候条件1阻挡掉的那些边,再加上P c上从dev c的前一个点到dev c的那条边。
    比如在图2中,我们发展的P c为1 -> 4 -> 2 -> 5这条路径的时候,立刻就知道了dev c是2号点。在发展它时,条件1把边(4, 5)和(4, 3)阻挡了,再加上P c上的(4, 2)这条边,那么将来在P c上从4号点发展新的候选路径时,条件1需要阻挡的3条边现在都知道了。
    我把上述的偏离点和条件1要阻挡的边也存在了每条路径的数据结构里,这样每次发展候选路径时,我就不用为了找这些信息而费劲地去遍历T i了。

    知道了条件1需要阻挡的边,我们就可以用一个布尔型二维数组来标记它们。在求P ivt时,就可以时刻检查有没有经过被阻挡的边了。

     三、实现条件2 以及 连接Pisv和Pivt

    有一个比较巧妙的方法可以既实现条件2,又快速地把P isv和P ivt连接起来。

    在求最短路径的算法中,不管用什么算法,我们都需要两个数组: d[]记录起点到每个点的最短距离; pre[]记录每个点在最短路径中的前驱点。

    我求P ivt用的是Dijkstra算法(正好这题中规定了所有边长都是正数,因此可以用Dijkstra)。我们知道在Dijkstra算法中,我们要管理一个集合Q,其中是还没有确定最短路径的点。初始情况下所有点都在集合Q中 ,除了d[s]为0,其它所有点的 d[]值都为无穷大。在Dijkstra算法中,每一次迭代时都从Q中取出 d[]值最小的点,再从它作一系计算,这里就不详说了。

    现在为了使条件2阻挡的点不被包含在P ivt中,我们可以在初始化的时候就把所有位于P isv上的点的 d[]值和 pre[]值都按照P isv的样子预设好,然后把除了v以外的所有位于P isv上的点都从Q中删除(我用一个布尔数组表示每个点是否属于集合Q),然后再做Dijkstra。这样求出的路径不仅在P ivt上实现了条件2,而且最终的结果自然就把P isv和P ivt连接起来了。

    这里还要讲一下我的一个教训。我一开始没有用Dijkstra算法,而是写了一个简单的BFS来求P ivt,实现条件2的方法和上面差不多。但是结果超时。后来我才发现,在BFS中每次有点要进队列的时候,一定要判一下队列里是不是已经有这个点了。我就是因为没有判重,结果做了很多重复的计算。

     四、求按字典序排名最前的路径

    首先把整张图反过来,也就是所有从a到b的边都变成从b到a。把题目中给出的起点和终点也反一下。这样求出的所有路径和在原图中求出的路径都是等价的,只是都反了一下。

    在求最短路径的过程中,我们都会用到relax操作。对于一条边(a, b),设它的长度为w(a, b),则relax(a, b)表示:

if ( d[a] + w(a, b) < d[b] ) {
    d[b] = d[a] + w(a, b);
    pre[b] = a;
}

    现在我们在relax中再追加一项操作:

if ( d[a] + w(a, b) == d[b] && a < pre[b] ) {
    pre[b] = a;
}

    这样我们最终求出的最短路径就是反向的字典序最先的路径,因为我们让每个点的前驱点的编号都尽量地小。再加上我们本来就把整张图反过来了,所以我们就得到了正向的字典序最先的路径。

    但是这个算法似乎只适用于没有长度为0的环的图。比如图3所示的这张图中, 点1和点2构成了一个长度为0的环。那么这时从1到3的字典序最先的最短路径是1 -> 2 -> 3,还是1 -> 2 -> 1 -> 2 -> 3,还是1 -> 2 -> 1 -> 2 -> 1 -> 2 -> 3?这时我们只能说字典序最先的最短路径不存在了。



图 3

    到这里我算是写完了。难免有讲得不清楚的地方,对于许多细节问题,还是请看 我的代码吧。


相关文章:

我很懒的  @ 2007-12-08 10:37

    首先,为了说话方便,列出一些术语:

    在 启发式搜索中,对于每个状态 x,启发函数 f(x) 通常是这样的形式:

f(x) = g(x) + h(x)

    其中 g(x) 是从初始状态走到 x 所花的代价;h(x) 是从 x 走到目标状态所需要的代价的 估计值

    相对于 h(x),还有一个概念叫 h*(x),表示从 x 走到目标状态所需要的 实际最小代价(当然,这个值有时我们是事先无法知道的)。

    如果在你的启发函数里,能保证  h(x) <= h*(x),也就是说,你不能高估了从 x 走到目标状态所需要的代价,那就可以说这个搜索是  A* 算法(这里的“*”,英文就读作 star)。

    A* 算法的特点是,如果存在从初始状态走到目标状态的 最小代价的解,那么用 A* 算法搜索时, 第一个找到的解就一定是最小代价的。这就是所谓的 可采纳(admissible)。

1. 求前 K 短的 可以带环的 路径(的长度)

     1.1. 典型的启发式搜索

    设起点为 s;终点为 t;对于一个点 v,dt(v) 表示从 v 走到 t 的 最短路径的长度(可以在初始化的时候全都算好)。

    网友  richard 教会了我,可以用最典型的启发式搜索来解这个问题。一个状态 x 表示的是从 s 走到某个点的一条路径,把这个点记作 x.v,把这条路径的长度记作 x.len。接着,我们可以使用以下启发函数:

g(x) = x.len;  h(x) = dt(x.v);
∴ f(x) = g(x) + h(x) = x.len + dt(x.v)

    初始状态中, x.v = s; x.len = 0。然后每次让优先队列(所谓的  Open 表)中 f(x) 值最小的状态 x 出队,再跟据图中所有从 x.v 出发的边发展下一层状态,让它们进队列。优先队列中不存在判重复的问题,因为每个状态所代表的路径肯定是不一样的。

    不难想通,这是一个 A* 算法,因为这里的 h(x) 本身就是 h*(x),当然满足 h(x) <= h*(x)。因此可以说,在每次出队列的状态 x 中,第一次遇到 x.v == t 时,就找到了从 s 到 t 的第一短的路径,它的长度就是 f(x)……第 k 次遇到 x.v == t 时,就找到了从 s 到 t 的第 k 短的路径。

     1.2. Yen 算法

    我从《 The K shortest paths problem》这篇文章中学到了另一个算法,名叫 Yen 算法(Yen 是发明者的名字)。它和上面讲的典型的 A* 算法使用相同的启发函数,但是状态的含义以及扩展状态的方式不同。

    在 Yen 算法中,状态 x 不仅可以代表从 s 走到 x.v 的一条路径(记作 P sv),更代表了一条从 s 到 t 的完整的路径,也就是 P sv 再连接上 从 x.v 到 t 的 最短路径。这一整条路径(记作 P x)的长度就是我们的启发函数 f(x)。

    在每个状态 x 中,还需要保存 x.v 在 P sv 中的前一个点,我们记作 x.pre。边 x.pre -> x.v 就称作 P x偏离边deviation edge); P x 上从 x.pre 到 t 的这一段子路径就称为 P x 的 偏离路径deviation path)。为什么叫作偏离路径,看到后面都明白了。

    先求出从 s 到 t 的最短路径,它就是初始状态 x 1 所要代表的路径。设它的第一条边是 s -> a,则  x 1.v = a; x 1.len = w(s, a) (w(s, a) 表示边 s -> a 的长度);  x 1.pre = s,也就是说,规定 P x1 的偏离边是 s -> a。

    把 x 1 放进优先队列。接下来,每当进入最大的循环的第 i 轮,从优先队列里出队的状态(启发值最小的,也就是路径长度目前最短的状态,记作 x i)就代表了第 i 短的解。第一轮出队的当然是前面定义的初始状态 x 1。下面要从它发展新的状态,作为可能的第 2 短的解,放进优先队列。发展的方法如下:

    对于 P x1 的偏离路径上的每一条边(设它为 u -> v),都要找出另一条边 u -> v',满足在所有从点 u 出发的边当中, w(u, v') + dt(v') 仅仅高于 w(u, v) + dt(v) (或与它相同);也就是说,从 u 出发,走 u -> v 这条边到终点是最近的,走 u -> v' 这条边是第 2 近的(或者一样近)。从每一条 u -> v',我们都可以发展出一个新状态 x': x'.v = v'; x'.len = w(P su) + w(u, v'); x'.pre = u,也就是说 P x' 的偏离边就是 u -> v'。



图 1
    图 1 给出了一个例子。假设图中蓝色和黑色的边组成的路径就是 P x1,蓝色边是它的偏离路径;那些红色的边就是前面说的那些 u -> v';红色的虚线就代表了从每个 v' 到 t 的最短路径。可见,每条 P x' 都是从 u -> v' 开始从 P x1 “身上” 偏离出来的,因此把 从偏离边到终点 的这一段路径称为 P x' 的 偏离路径

    注意,由于本问题中求的路径是 可以带环的,所以走到终点以后还可以回头再走。因此,在图 1 中可以看到在点 t 后面也发展了一条偏离路径。这条偏离路径显然不再需要是第 2 短的,而是从 t 出发再回到 t 的最短的路径。

    上面讲的是从 x 1 发展状态的情况。从之后的 x i 发展状态的时候还有一点要注意:在我们寻找偏离边 u -> v' 的时候,如果 u == x i.pre (也就是当 要找的偏离边 和 x i 的偏离边 是从同一点出发时),则要注意 u -> v' 不仅要和 u -> x i.v 不同,而且要和 x i 的所有祖先状态中从点 u 出发的那条边都不同,不然新发展的状态岂不是和 x i 的祖先状态重复了。



图 2
    图 2 给出了一个例子。假设蓝色路径是从黑色路径中发展出的偏离路径;当从蓝色路径发展偏离路径时,要找的是除了蓝色和黑色的边以外,能以最短的距离走到 t 的那条边,假设这里我们找到的是红色的那条边;当从红色路径发展偏离路径时,要找的是除了红色、蓝色和黑色的边以外,能以最短的距离走到 t 的那条边,假设这里我们找到的是绿色的那条边。

    如此一来,可能有很多偏离路径都是从同一点偏离出来的,但是它们的偏离边都不相同。要在程序中实现这一点,可以在每个状态中记录下所有祖先状态的偏离边。

    显然 Yen 算法也是一个 A* 算法,但是它有一个特点,前面已经说过了,就是最大的那个循环最多只要做 K 次,因为每当一个状态出队列时,我们就找到了一个解。因此基本上可以估计出算法的时间复杂度:
  • 设图中有 N 个点,那么一条偏离路径上最多只有 N 条边(因为它是一条边 加上 从某一点到终点的最短路径),也就是说,从一个状态最多发展出 N + 1 个新状态(偏离路径上的每条边发展出一个,从点 t 再发展出一个)。
  • 寻找一条偏离路径时,需要扫描从一个点出发的所有边,暂且假设从一个点出发的边最多也是 N 条,那么这一步要花 O(N) 的时间。
  • 可以想象优先队列(Open 表)里最多有 O(K * N) 个元素,所以每次维护优先队列的时间差不多是 O( lg (K * N) )。
     因此,总的时间复杂度,在最差情况下,差不多就是  O( K * ( N2 + lg(K * N) ) )。当然这只是我个人估计一下,不要太拿它当回事。

     1.3. MPS 算法

    同样是在《 The K shortest paths problem》这篇文章中,还介绍了作者自已发明的 MPS 算法(MPS 是该文章的三位作者的名字缩写)。它的框架和 Yen 算法相同,但是有一个优化,可以加快寻找偏离边的速度。方法就是把从每个点出发的所有边,都按照从该条边走向 t 的最短距离 升序排序(最好用邻接链表描述图)。



图 3
    图 3 给出了一个例子。图中从点 s 出发的边有红、蓝和绿三条,延着它们到达终点 t 的最短距离分别为 3、 2 和 4。因此把从 s 出发的边排序为 (蓝, 红, 绿)。

    这样一来,寻找偏离边的时间就只有 O(1) 了。因为我们从某一点第一次发展偏离边时,只要选它的邻接链表中的第一条边;下一次再从该点发展时,只要选第二条边……再也不用一一扫描所有边了,也不用担心会和祖先状态的偏离边重复了。

    假设图中有 N 个点,从每个点出发的边最多也是 N 条。那么排序一个点的邻接链表需要 O(N * lg N) 的时间,排序整个邻接链表的时间就是 O(N 2 * lgN);搜索的时间由 Yen 算法的 O(K * N 2) 降至 O(K * N)。因此,整个算法在最差情况下的时间复杂度大约就是 O(N 2 * lgN + K * N)。(从数字上看,好像也没有比 Yen 算法快到哪去 ……但是实际试下来确实是快的。)

2. 求前 K 短的 无环 路径(的长度)

     2.1. 典型的启发式搜索

    网友  richard 在他的 这篇文章里介绍了,把  1.1 节中的算法稍加修改,就可以用来求 无环的前 K 短路径。修改方法就是在每个状态中保存 P sv 所经过的点;当从一个状态发展新状态时,下一步走的点不能出现在 P sv 中(如果点比较少的话,用位运算就可以很快地对此进行判断。)。这样一来,最终求出的路径就无环了。



图 4
    这个算法在大多数情况下确实很好用,但是在  2006 年横滨赛区的最后一题中,就有一组阴险的数据可以让这个算法超时。如图 4 所示,从点 s 出发只有两条边:蓝色的很短,红色的非常长(既使把图中所有边的长度都加起来,也没有它长);能走向点 t 的只有一条边,它的起点正是蓝色边的终点;图的其它部分有很多点,它们两两之间都有边(图 4 中只是象征性地画了一下,实际上有更多点)。可以想象,只要第一步走了蓝色的边,那么能到达点 t 的无环的路径只有一条,那就是 s -> 蓝色的点 -> t。从第 2 短的解开始,都必须走红色的边。

    但是启发式搜索一定会先走蓝色的边,然后尝试其后的所有路径。直到实在走投无路径时,才会回过头来走红色的边,因为从长度来看,红色边的优先级实在太低了(虽然它才是正解)。假设图中有 N 个点,可以想象,启发式搜索会先尝试 O(N!) 条错误的路径,那就太可怕了。

    我曾经想过下面一些优化的方案,但是好像都行不通:
  • 如果在一个我们想要发展的新状态 x 中,从 s 到 x.v 的路径 和 从 x.v 到 t 的最短路径上有重复的点(前者的点集被保存在 x 中;后者的点集可以在初始化所有最短路径时记录下来),则不让它进优先队列(Open 表)?

        这样做是不对的。因为虽然从 x.v 到 t 的最短路径不能构成无环的解,但是这并不代表从 x.v 到 t 的稍长一点的路径就不可能构成无环的解。因此,这样的状态还是必须得发展下去,否则就可能错过了一些解。

  • 在优先队列(Open 表)里只保存前 K 个最优的状态,比较差的状态就不进队列了?

        这样做更加是不对的。因为图 4 这个例子就已经明确地说明了,启发值比较优先的状态并不一定能通向正解,而启发值较差的状态说不定就能通向正解。
     总之,目前在我看来,典型的启发式搜索对于图 4 中的这种情况真的是没辙。当然,我希望  richard能够反驳这个论点。

     2.2. Yen 算法

    Yen 算法的无环版本我在 这篇文章里已经写过了。其思想和它的可以带环的版本相同,只是在找偏离路径的时候,不能再用初始化求好的现成的最短路径了,因为它们可能无法构成无环的解;而是要当场求一条最短路径,在求的过程中屏蔽掉前半条路径经过的点,以保证整条路径无环。

    由于 Yen 算法的无环版本在找偏离路径时,不再是扫描从一个点出发的所有边,而是运行一次最短路径算法。所以它的最差时间复杂度 由可以带环版本的  O( K * ( N2 + lg(K * N) ) ) 升至  O( K * ( N3 + lg(K * N) ) )

     2.3. MPS 算法

    作者还是 M、 P 和 S 这三个人,在《 The K shortest loopless paths problem》这篇文章中介绍了 MPS 算法的无环版本。它和 MPS 算法的可以带环版本基本相同,只是在最大的循环中,每当一个状态出队列时,判断它是否无环,如果无环才算找到一个解。当然,不管出队的状态有没有环,都需要从它发展新状态,原因在  2.1 节中已经说过了。

    这个算法在一般情况下(比如随机生成的图)会比 Yen 算法的无环版本快很多,毕竟它在寻找偏离路径上有很大的时间优势。但是它的致命伤和典型的启发式搜索一样,就是像图 4 那样的情况。因为它在决定偏离路径时,还是以启发为主,并不能确定找到的是正解,我就不多说了。

    就写到这里吧。最后总结一下,上面介绍的求前 K 短路径的各种算法,不管是有环的版本还是无环的版本,都是 A* 算法。正因为这样,它们才能保证能依次求出前 K 短的解。最后面好像写得有点潦草,但是我觉得足以说明问题了。跟本文有关的题目,我知道的还有  UVA 10740 和  PKU 2449



次短路径与次小生成树问题的简单解法

[次短路径]

次短路径可以看作是k短路径问题的一种特殊情况,求k短路径有Yen算法等较为复杂的方法,对于次短路径,可以有更为简易的方法。下面介绍一种求两个顶点之间次短路径的解法。

我们要对一个有向赋权图(无向图每条边可以看作两条相反的有向边)的顶点S到T之间求次短路径,首先应求出S的单源最短路径。遍历有向图,标记出可以在最短路径上的边,加入集合K。然后枚举删除集合K中每条边,求从S到T的最短路径,记录每次求出的路径长度值,其最小值就是次短路径的长度。

在这里我们以为次短路径长度可以等于最短路径长度,如果想等,也可以看作是从S到T有不止一条最短路径。如果我们规定求从S到T大于最短路径长度的次短路径,则答案就是每次删边后大于原最短路径的S到T的最短路径长度的最小值。

用Dijkstra+堆求单源最短路径,则每次求最短路径时间复杂度为O(Nlog(N+M) + M),所以总的时间复杂度为O(NM*log(N+M) + M^2)。该估计是较为悲观的,因为一般来说,在最短路径上的边的条数要远远小于M,所以实际效果要比预想的好。

[次小生成树]

类比上述次短路径求法,很容易想到一个“枚举删除最小生成树上的每条边,再求最小生成树”的直观解法。如果用Prim+堆,每次最小生成树时间复杂度为O(Nlog(N+M) + M),枚举删除有O(N)条边,时间复杂度就是O(N^2log(N+M) + N*M),当图很稠密时,接近O(N^3)。这种方法简易直观,但我们有一个更简单,而且效率更高的O(N^2+M)的解法,下面介绍这种方法。

首先求出原图最小生成树,记录权值之和为MinST。枚举添加每条不在最小生成树上的边(u,v),加上以后一定会形成一个环。找到环上权值第二大的边(即除了(u,v)以外的权值最大的边),把它删掉,计算当前生成树的权值之和。取所有枚举修改的生成树权值之和的最小值,就是次小生成树。

具体实现时,更简单的方法是从每个节点i遍历整个最小生成树,定义F[j]为从i到j的路径上最大边的权值。遍历图求出F[j]的值,然后对于添加每条不在最小生成树中的边(i,j),新的生成树权值之和就是MinST + w(i,j) - F[j],记录其最小值,则为次小生成树。

该算法的时间复杂度为O(N^2 + M)。由于只用求一次最小生成树,可以用最简单的Prim,时间复杂度为O(N^2)。算法的瓶颈不在求最小生成树,而在O(N^2+M)的枚举加边修改,所以用更好的最小生成树算法是没有必要的。


#include <iostream>
#include <cstring>//memset.
#include <vector>//representing paths.
#include <queue>//priority_queue.
#include <algorithm>//reverse, lexicographical_compare.
#include <functional>//greater.

using namespace std;

typedef char VT;//The type of vertex numbers.
typedef int LT;//The type of lengths of edges.

const VT MAX_VER = 50;
const LT MAX_LEN = 99999999;

struct Edge {
    VT to;
    LT len;
    Edge* next;

    Edge(VT t = 0, LT l = 0, Edge* n = NULL) {
        to = t; len = l; next = n;
    }
};

struct Path {
    vector<VT> node;
    vector<VT> block;
    LT len;

    //The index of the deviation node. Nodes before this node are on the
    //k shortest paths' tree.
    VT dev;

    //Initialize a path with only one vertex.
    Path(VT v = 0) : node(), block() { node.push_back(v); len = 0; }

    bool operator > (const Path& p) const {
        return len > p.len || len == p.len
               && lexicographical_compare(
                      p.node.rbegin(), p.node.rend(),
                      node.rbegin(), node.rend() );
    }
};

ostream& operator << (ostream& os, const Path& p) {
    os << p.node[ p.node.size() - 1 ] + 1;
    for (int i = p.node.size() - 2; i >= 0; i--) {
        os << "-" << p.node[i] + 1;
    }
    return os;
}

class Graph {
public:
    Graph() { memset(m_adj, 0, sizeof(m_adj)); }
    ~Graph() { init(); }

    //Multiple edges between same pair of vertices are not allowed.
    void addEdge(VT from, VT to, LT len) {
        if ( NULL == m_edge[from][to] ) {
            m_adj[from] = new Edge(to, len, m_adj[from]);
            m_edge[from][to] = m_adj[from];
        }
    }

    //Can NOT handle graphs with NEGATIVE edges!!!
    void dijkstra() {
        int minV;
        for (int iter = 0; iter < m_verCnt; iter++) {
            minV = -1;
            for (int i = 0; i < m_verCnt; i++) {
                if (!m_visit[i]
                    && ( -1 == minV || m_sh[i] < m_sh[minV] )
                   ) {
                    minV = i;
                }
            }
            if (-1 == minV) { break; }
            m_visit[minV] = true;
            for (Edge* adj = m_adj[minV]; adj; adj = adj->next) {
                VT to = adj->to;
                //The condition "!m_visit[to]" is suitable
                //only for non-negative-edge graphs.
                if (!m_visit[to] && !m_block[minV][to]) {
                    relax(minV, to, adj->len);
                }
            }
        }
    }

    //Initialize a graph with "verCnt" vertices and no edge.
    void init(VT verCnt = MAX_VER) {
        memset( m_edge, 0, sizeof(m_edge) );
        m_verCnt = verCnt;
        Edge* p, *temp;
        for (VT i = 0; i < m_verCnt; i++) {
            p = m_adj[i];
            while (p) { temp = p; p = p->next; delete temp; }
            m_adj[i] = NULL;
        }
    }

    //Get the k loopless shortest paths with YEN's algorithm.
    //If two paths have the same length, the one whose reversed path
    //has lexicographically lower value ranks first.
    vector<Path> yenLoopless(VT source, VT sink, int k) {
        vector<Path> result;
        priority_queue< Path, vector<Path>, greater<Path> > candidate;
        memset(m_block, 0, sizeof(m_block));
        initSingleSrc(source);
        dijkstra();
        if ( shortest(sink) < MAX_LEN ) {
            Path sh = shortestPath(sink);
            sh.dev = 1;
            sh.block.push_back( sh.node[sh.dev] );
            candidate.push(sh);
        }
        while ( result.size() < k && !candidate.empty() ) {
            Path p = candidate.top();
            candidate.pop();
            VT dev = p.dev;
            while ( dev < p.node.size() ) {
                VT pre = p.node[dev - 1];
                if (dev == p.dev) {
                    for (int i = 0; i < p.block.size(); i++) {
                        m_block[pre][ p.block[i] ] = true;
                    }
                }
                else { m_block[pre][ p.node[dev] ] = true; }
                initSingleSrc(source);
                delSubpath(p, dev);
                dijkstra();

                if (shortest(sink) < MAX_LEN) {
                    Path newP = shortestPath(sink);
                    newP.dev = dev;
                    if (dev == p.dev) { newP.block = p.block; }
                    else { newP.block.push_back( p.node[dev] ); }
                    newP.block.push_back( newP.node[dev] );
                    candidate.push(newP);
                }

                dev++;
            }
            memset(m_block, 0, sizeof(m_block));
            result.push_back(p);
        }
        return result;
    }

    Path shortestPath(VT v) const {
        Path p(v);
        p.len = m_sh[v];
        for (v = m_pre[v]; -1 != v; v = m_pre[v]) {p.node.push_back(v);}
        reverse( p.node.begin(), p.node.end() );
        return p;
    }

    //The shortest distance from the source to v
    //(after solving the single source shortest paths).
    LT shortest(VT v) const { return m_sh[v]; }

private:
    //Determine leading subpath of the shortest path generated by
    //dijkstra().
    void delSubpath(const Path& p, VT dev) {
        VT pre = p.node[0];
        m_visit[pre] = true;
        int v;
        for (VT i = 1; dev != i; i++) {
            v = p.node[i];
            m_pre[v] = pre;
            m_sh[v] = m_sh[pre] + m_edge[pre][v]->len;
            m_visit[v] = true;
            pre = v;
        }
        m_visit[pre] = false;
    }

    //Initialize the single source shortest path algorithms.
    void initSingleSrc(VT source) {
        for (VT i = 0; i < m_verCnt; i++) {
            m_sh[i] = MAX_LEN;
            m_pre[i] = -1;
            m_visit[i] = false;
        }
        m_sh[source] = 0;
    }

    //Help the shortest path algorithms.
    bool relax(VT from, VT to, LT len) {
        if (m_sh[to] > m_sh[from] + len) {
            m_sh[to] = m_sh[from] + len; m_pre[to] = from; return true;
        }
        //With the following condition, the REVERSE shortest path with be
        //the LEXICOGRAPHICALLY first one.
        else if (m_sh[to] == m_sh[from] + len && from < m_pre[to]) {
            m_pre[to] = from; return true;
        }
        return false;
    }

    VT m_verCnt;//Number of vertices.
    Edge* m_adj[MAX_VER];//Adjacent list.
    LT m_sh[MAX_VER];//Every vertex's shortest distance from the source.
    VT m_pre[MAX_VER];//The previous vertex in the shortest path.
    //m_edge[i][j]: the edge from i to j. NULL value if no edge (i, j).
    Edge* m_edge[MAX_VER][MAX_VER];
    bool m_visit[MAX_VER];//Help the dijkstra.

    //Help to make acyclic paths.
    bool m_block[MAX_VER][MAX_VER];
};

//To get the lexicographically first shortest path,
//I reverse the graph. See the input() function.
Graph g_revGraph;
int g_k;
int g_source;
int g_sink;

bool input() {
    int verCnt;
    int eCnt;
    cin >> verCnt >> eCnt >> g_k >> g_source >> g_sink;
    g_source--; g_sink--;//I number the vertices from 0 to n - 1.
    if (0 == verCnt) { return false; }
    g_revGraph.init(verCnt);
    int from, to, len;
    for (int i = 0; i < eCnt; i++) {
        cin >> from >> to >> len;
        from--; to--;
        g_revGraph.addEdge(to, from, len);//Edges are reversed.
    }
    return true;
}

void solve() {
    //The source and sink are reversed in the reversed graph.
    vector<Path> kSh = g_revGraph.yenLoopless(g_sink, g_source, g_k);
    if (kSh.size() < g_k) { cout << "None" << endl; }
    else { cout << kSh[g_k - 1] << endl; }
}

int main() {
    while ( input() ) {
        solve();
    }
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值