图论中的优先级搜索——DFS,BFS,Prim,Dijkstra
DFS非常简单,在搜索的过程中一旦遇到一个未标记的顶点,就沿着这个顶点继续搜索,直至遍历完所有可达的顶点。DFS具有回溯的性质,由此形成的搜索树很瘦很高。执行DFS时,同时会维护一个时间戳(time-stamp),以此记录每个顶点被搜索到的顺序,DFS形成的搜索树包含不同类型的边(树边,回边,下边以及交叉边),边的性质能通过时间戳识别出来。
BFS则相反,遇到一个未搜索的顶点时,先将与该顶点邻接的搜索完,然后进入下一轮搜索。与源点距离为k+1的顶点总是在所有与源点距离小于等于k的所有顶点都搜索完之后才被遍历。对于不区分边权值的图,BFS能计算出最短路径——DFS对此无能为力。
Prim算法是计算最小生成树(minimum
Dijkstra算法可以解决非负权值的单源最短路径问题(shortest-paths
仔细观察可以发现,这4种图搜索算法都具有相同的模式:首先它们都遍历了所有顶点,并且在遍历的过程中维护了一个顶点集,搜索一个顶点时把相邻的顶点按照某种规则加入到这个集合中,搜索下一个顶点时再按照某种规则从这个顶点集中取出顶点;取出顶点的规则是第二个共同点——它们都是贪心算法,在选择下一个顶点的时刻都依据某种优先级,选择优先级最大的顶点。
换言之,DFS、BFS、Prim算法和Dijkstra算法其实都是优先级优先搜索(priority-first
既然这4种图搜索算法都有共同的思想,理所当然的,它们具有共同的代码接口(蓝色的函数调用包含与优先队列相关的操作):
void
int
Update(v);
Link
for
}
}
优先级搜索的算法接口基于图的邻接表(adjacency
typedef
struct
typedef
struct
GraphPFS函数完整地包含了优先级搜索的模式,只需要按照算法的要求选择合适的数据结构作为PQ,就能利用GraphPFS完成DFS、BFS、Prim算法和Dijkstra算法。
Depth-first
大动干戈地用优先级队列实现DFS,无异于大炮打蚊子。但这个例子非常经典,所以我忍不住开一炮~
DFS中的优先级是什么呢?DFS总是选择刚搜到的顶点进行下一轮搜索,显然,最近加入队列的顶点具有最高的优先级。不多想就能想到可以用栈(stack)作为DFS的PQ:栈总能访问到最近加入的元素。
#define
}
#define
#define
#define
#define
#define
#define
#define
为了记录顶点的时间戳(time-stamp),增加了一个辅助数组ts,ts[v]
不过要注意的是,DFS的PFS实现与直接递归实现有一些区别,递归DFS会在发现一个新顶点的时候立即进一步搜索,PFS是在遍历完邻接顶点后才选择下一个顶点,所以ts数组记录的时间戳会不一样。比如以0为源点遍历下面这个图:
PFS的时间戳(time-stamp,ts)
vertex | 0 | 1 | 2 | 3 | 4 | 5 |
ts | 0 | 4 | 5 | 3 | 1 | 2 |
直接递归的时间戳
vertex | 0 | 1 | 2 | 3 | 4 | 5 |
ts | 0 | 1 | 2 | 3 | 4 | 5 |
虽然PFS与递归搜索的路径有所不同,但它仍然是正宗的深度优先搜寻,栈的结构确保了它总是优先沿着更深的路径搜索。
Breadth-first
在BFS中,我们希望离源点近的顶点先被搜索,然后再搜索那些离源点更远的顶点。这要求先加入PQ的顶点要先被搜索,只要利用队列(queue)先进先出(first
#define
}
#define
#define
#define
#define
#define
}
#define
#define
BFS的结果以父链接表示的形式保存在pl(parent
vertex | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
pl | 0 | 0 | 0 | 0 | 1 | 2 | 3 |
父链接表示最终结果
BFS还可以获取到许多信息,比如顶点到源点之间经过的边数,以及BFS的时间戳等等,只要向PQinsert和Update内加入相应的代码即可获取。
Prim's
DFS和BFS几乎是最简单的图算法,现在来个稍微高级一点的——最小生成树(minimum
Prim算法计算MST的过程中维护了一个割(cut),区分了树(tree)顶点和非树(nontree)顶点,树顶点即是被选入MST的顶点。显然的,每一次从PQ中取出的顶点应该都是树顶点,关键在于:如何确定顶点的优先级(priority)?
由于Prim算法的框架是每次选择距离树最近的非树顶点,选择V-1次之后计算结束,因此顶点的优先级理所应当的由非树顶点与树顶点之间的距离决定,距树最近的非树顶点具有最大的优先级。
确定了优先级之后,接下来要做的就是挑选合适的数据结构充当PQ。事实上我们有许多选择:无序数组、有序数组、自组织链表,都可以。但注意到队列操作的效率直接决定了整个算法的效率,我们应当选择能够快速插入和搜索最大值的数据结构,显然堆(heap)可以胜任。注意各顶点的优先级不是一开始就明确的,而是可能在搜索过程中发生变化,所以堆还必须具有修改元素优先级的功能。普通的堆没有这样的功能,而索引堆(即以目标数组的索引作为元素的堆)可以轻松实现这个功能,所以我们选择索引堆作为PQ(P.S.
好了,PQ的数据结构也选好了,接下来考虑如何表示Prim算法的中间过程和结果。
首先,Prim算法计算的是MST,表示一棵树的方式在BFS中已经介绍过一种,既父链接表示。这种表示非常方便更新,所以在Prim算法中同样使用一个pl(parent
其次,刚才提到索引堆的元素是目标数组的索引,目标数组涉及到顶点的优先级,因此目标数组的元素即是各个非树顶点到MST的距离。由于边权值是double型,所以用一个double的dist(distance)数组作为索引堆的目标数组。
最后,在计算MST的每一步中,都要知道当前与每个非树顶点最近的树顶点,才能从其中选出所有非树顶点中与MST最近的的一个。这些信息保存在fr(fringe)数组中。换言之,fr存放了阶段性的搜索结果。
以上这些信息的保存方式与PQ一样有许多选择,但最简单的实现是使用顶点索引数组。OK,现在可以上代码了。
#define
#define
}
#define
#define
#define
#define
#define
}
#define
#define
}
第一行用P表示优先级,即树顶点到非树顶点直接的距离t->wt,可以简化代码。PQinit重置了pl和fr,并设置了PQ的目标数组(priority
算法的重点在于PQinsert和PQchange。在搜索顶点的过程中,如果一个顶点w是通过v首次被搜索到(w
经过V-1次迭代,所有的顶点最终都会成为树顶点。
Dijkstra's
伟大的Dijkstra又登场了。
Dijkstra算法用于计算无负权值图的单源最短路径,与Prim算法一样,我们从PFS出发,一一解决Dijkstra算法中将要遇到的问题。
既然是求最短路径,优先级当然由顶点到源点的路径长度决定,路径长度越短,优先级越高。与Prim算法一样,PQ仍然可以用索引堆实现。
与Prim算法一样的还有最终结果的表示。Dijkstra算法计算最短路径,最终结果应该完整地包含这两个信息:一、最短路径怎么走;二、最短路径的长度。第一个信息,首先最短路径肯定不会包含环,所以可以用最短路径树(shortest-paths
算法的运行方式如同文章开头所写,维护一棵SPT,最开始只有源点。随后逐步向SPT中添加非树顶点,规则是每一次都选择距离源点最近的非树顶点。遍历顶点的同时,反复地执行一项被称为松弛(relaxation)的操作——对于顶点v,每当遇到一个比已知从源点到达v的最短路径还要短的路径,就更新关于v的路径信息(其实我觉得“松弛”这个术语很不恰当,一旦遇到更短的路径,那么这条路径应该更短更紧才对,怎么会是“松弛”呢?)。松弛是在遍历顶点的过程中进行的,从PQ中取出V-1个顶点、或所有可达顶点后,算法结束。
#define
#define
}
#define
#define
#define
#define
#define
#define
#define
}
Dijkstra算法同样定义了一个P,即源点到目标顶点的距离,方便写代码。注意算法初始化PQ的时候就已经把所有顶点都插入索引堆(为什么?),因此PQinsert也不需要做任何事情,这也是Dijkstra算法与Prim算法的一个区别。刚才提到Dijkstra算法在计算完所有从源点可达的顶点后结束,此处不可达的顶点以距离MAX_WT表示,所以Update做的事情并不是更新什么信息,而是判断当前是否遇到了一个不可到达的顶点,如果是,那么算法也没有继续进行下去的必要了。初始情况下,顶点到自己的距离为0,其它所有顶点都不可达,随着松弛操作的进行,其它顶点的正确路径信息也会慢慢明晰,所有的松弛操作都在PQchange内完成。
Dijkstra算法与Prim算法是如此的相似,所以把它们对比一下是件有趣的事情。它们的相通之处贯穿全文:优先级优先搜索的本质。然而它们有个明显的区别,即刚才提到的:Prim算法在计算过程中多维护了一个fr数组,记录每一步当前与每个非树顶点最近的树顶点,每当遇到一个未曾搜索过的顶点就加入到fr。
之所以要这样做是因为Prim算法必须区分树顶点和非树顶点,Prim的优先级取决于非树顶点与树的距离,必须在计算过程中保存这些中间结果——每个非树顶点所关联的最近的树顶点。Dijkstra算法不需要,所以一开始就把所有顶点插入到PQ中,然后不断地松弛。其实Dijkstra算法也要做出某种区分,然而它只需要区分源点和非源点……
无论是“看起来不太像”的DFS和BFS,还是“看起来很像”的Prim算法和Dijkstra算法,都通过优先级优先搜索(PFS)这一算法思想联系到一起。现在再度总结一下:PFS是一种搜索图(Graph)的一般性算法,它先将图中的顶点(部分或全部地)加入到一个广义的优先队列中,然后根据特定的优先级,每次从队列里选取一条优先级最高的边,加入到特定的搜索树。搜索树就是PFS的最终结果(如最小生成树、最短路径树、DFS树……)。由此,DFS、BFS、Prim算法和Dijkstra算法都表征出相同的思想:
算法 | 优先级/P | 优先队列(PQ) | 结果 |
DFS | 逆先序 | 栈 | DFS |
BFS | 先序 | 队列 | SPT |
Prim | 边权值/t->wt | 堆 | MST |
Dijkstra | 路径长度/dist[v] | 堆 | SPT |
其实,你可以选择任何你想得到的优先级对图进行搜索,也许会得到意想不到的结果哦~