对于前K短路径问题 和 A*算法 的一些小小总结
来源:互联网 发布:linux 获取命令返回值 编辑:程序博客网 时间:2024/05/21 20:23
在启发式搜索中,对于每个状态 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 的一条路径(记作 Psv),更代表了一条从 s 到 t 的完整的路径,也就是 Psv 再连接上 从 x.v 到 t 的最短路径。这一整条路径(记作 Px)的长度就是我们的启发函数 f(x)。
在每个状态 x 中,还需要保存 x.v 在 Psv 中的前一个点,我们记作 x.pre。边 x.pre -> x.v 就称作 Px 的偏离边(deviation edge); Px 上从 x.pre 到 t 的这一段子路径就称为 Px 的偏离路径(deviation path)。为什么叫作偏离路径,看到后面都明白了。
先求出从 s 到 t 的最短路径,它就是初始状态 x1 所要代表的路径。设它的第一条边是 s -> a,则 x1.v = a; x1.len = w(s, a) (w(s, a) 表示边 s -> a 的长度); x1.pre = s,也就是说,规定 Px1 的偏离边是 s -> a。
把 x1 放进优先队列。接下来,每当进入最大的循环的第 i 轮,从优先队列里出队的状态(启发值最小的,也就是路径长度目前最短的状态,记作 xi)就代表了第 i 短的解。第一轮出队的当然是前面定义的初始状态 x1。下面要从它发展新的状态,作为可能的第 2 短的解,放进优先队列。发展的方法如下:
对于 Px1 的偏离路径上的每一条边(设它为 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(Psu) + w(u, v'); x'.pre = u,也就是说 Px' 的偏离边就是 u -> v'。
图 1
注意,由于本问题中求的路径是可以带环的,所以走到终点以后还可以回头再走。因此,在图 1 中可以看到在点 t 后面也发展了一条偏离路径。这条偏离路径显然不再需要是第 2 短的,而是从 t 出发再回到 t 的最短的路径。
上面讲的是从 x1 发展状态的情况。从之后的 xi 发展状态的时候还有一点要注意:在我们寻找偏离边 u -> v' 的时候,如果 u == xi.pre (也就是当 要找的偏离边 和 xi 的偏离边 是从同一点出发时),则要注意 u -> v' 不仅要和 u -> xi.v 不同,而且要和 xi 的所有祖先状态中从点 u 出发的那条边都不同,不然新发展的状态岂不是和 xi 的祖先状态重复了。
图 2
如此一来,可能有很多偏离路径都是从同一点偏离出来的,但是它们的偏离边都不相同。要在程序中实现这一点,可以在每个状态中记录下所有祖先状态的偏离边。
显然 Yen 算法也是一个 A* 算法,但是它有一个特点,前面已经说过了,就是最大的那个循环最多只要做 K 次,因为每当一个状态出队列时,我们就找到了一个解。因此基本上可以估计出算法的时间复杂度:
- 设图中有 N 个点,那么一条偏离路径上最多只有 N 条边(因为它是一条边 加上 从某一点到终点的最短路径),也就是说,从一个状态最多发展出 N + 1 个新状态(偏离路径上的每条边发展出一个,从点 t 再发展出一个)。
- 寻找一条偏离路径时,需要扫描从一个点出发的所有边,暂且假设从一个点出发的边最多也是 N 条,那么这一步要花 O(N) 的时间。
- 可以想象优先队列(Open 表)里最多有 O(K * N) 个元素,所以每次维护优先队列的时间差不多是 O( lg (K * N) )。
1.3. MPS 算法
同样是在《The K shortest paths problem》 这篇文章中,还介绍了作者自已发明的 MPS 算法(MPS 是该文章的三位作者的名字缩写)。它的框架和 Yen 算法相同,但是有一个优化,可以加快寻找偏离边的速度。方法就是把从每个点出发的所有边,都按照从该条边走向 t 的最短距离 升序排序(最好用邻接链表描述图)。
图 3
这样一来,寻找偏离边的时间就只有 O(1) 了。因为我们从某一点第一次发展偏离边时,只要选它的邻接链表中的第一条边;下一次再从该点发展时,只要选第二条边……再也不用一一扫描所有边了,也不用担心会和祖先状态的偏离边重复了。
假设图中有 N 个点,从每个点出发的边最多也是 N 条。那么排序一个点的邻接链表需要 O(N * lg N) 的时间,排序整个邻接链表的时间就是 O(N2 * lgN);搜索的时间由 Yen 算法的 O(K * N2) 降至 O(K * N)。因此,整个算法在最差情况下的时间复杂度大约就是 O(N2 * lgN + K * N)。(从数字上看,好像也没有比 Yen 算法快到哪去……但是实际试下来确实是快的。)
2. 求前 K 短的 无环 路径(的长度)
2.1. 典型的启发式搜索
网友 richard 在他的这篇文章里介绍了,把 1.1 节中的算法稍加修改,就可以用来求无环的前 K 短路径。修改方法就是在每个状态中保存 Psv 所经过的点;当从一个状态发展新状态时,下一步走的点不能出现在 Psv 中(如果点比较少的话,用位运算就可以很快地对此进行判断。)。这样一来,最终求出的路径就无环了。
图 4
但是启发式搜索一定会先走蓝色的边,然后尝试其后的所有路径。直到实在走投无路径时,才会回过头来走红色的边,因为从长度来看,红色边的优先级实在太低了 (虽然它才是正解)。假设图中有 N 个点,可以想象,启发式搜索会先尝试 O(N!) 条错误的路径,那就太可怕了。
我曾经想过下面一些优化的方案,但是好像都行不通:
- 如果在一个我们想要发展的新状态 x 中,从 s 到 x.v 的路径 和 从 x.v 到 t 的最短路径上有重复的点(前者的点集被保存在 x 中;后者的点集可以在初始化所有最短路径时记录下来),则不让它进优先队列(Open 表)?
这样做是不对的。因为虽然从 x.v 到 t 的最短路径不能构成无环的解,但是这并不代表从 x.v 到 t 的稍长一点的路径就不可能构成无环的解。因此,这样的状态还是必须得发展下去,否则就可能错过了一些解。 - 在优先队列(Open 表)里只保存前 K 个最优的状态,比较差的状态就不进队列了?
这样做更加是不对的。因为图 4 这个例子就已经明确地说明了,启发值比较优先的状态并不一定能通向正解,而启发值较差的状态说不定就能通向正解。
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短路径问题 和 A*算法 的一些小小总结
- 对于 前K短路径问题 和 A*算法 的一些小小总结
- 对于 前K短路径问题 和 A*算法 的一些小小总结
- 最短路径问题的Dijkstra和SPFA算法总结
- 典型的求k短路径的问题,有很多种算法,暂时试了试Dijkstra+A*算法
- 第k短路径的学习,及A*算法的初步认识,以及usaco牛跑步,k短路径
- K最短路问题(单源点最短路径+A*算法)
- POJ 2449 Remmarguts' Date K最短路问题(单源点最短路径+A*算法)
- hdu 5102 树上前k短路径长度和
- 最短路径算法Dijkstra和A*
- 最短路径的一些算法
- A*算法的最短路径实现!
- 最短路径算法之AStar算法(二) A Star算法需要注意的问题
- 前K条最短路径算法
- 前k条最短路径算法
- 前K条最短路径算法
- 前K条最短路径算法
- 前 k 条最短路径算法
- sharepoint问题
- 郁闷死
- 未知类型转换
- 2449 Remmarguts' Date //K短路
- 经络3+1祛痘论坛8个方法告别痘痘困扰
- 对于前K短路径问题 和 A*算法 的一些小小总结
- RegisterHotKey
- 硬盘上安装linux
- POJ上的LCA问题小节(转)
- error LNK2001: unresolved external symbol _DrawDibOpen@0
- 参加java培训了
- 添加节点到单链表某个节点的前面
- 主键的创建
- Flex中让文本框自动填充值(数据来自外表)