这三个算法每本算法书都要讲到,这次看《算法之道》又复习了一遍,觉得有些新的领悟,写个模板记录一下。
Kruskal和Prim算法解决的问题都是最小生成树问题,即对于一个图G<V,E>,找到它的最小生成树T<V,E’>,其中E’包含于E,使得所有V都连通。Dijkstra算法解决的是单源多点最短路径问题,即对于一个图G<V,E>和一个起点S,为图中的其它所有节点找到距离S最近的路径。
所说的图都是加权图,如果是均权图或者权重为一的图,则不存在最小生成树这一概念,因为任意一棵生成树的大小都是一致的。所谓的单源多点最短路径也可以用BFS进行解答。
这三个算法的本质都是贪心,三个问题都具有贪婪选择性质使得使用贪心算法可以得到最优解。
先来看Kruskal算法。先描述算法再证明最优性。
建立两个边集,初始时Q为所有边的集合,A为空集,算法终结时A为所求的最小生成树。每次删掉Q中权重最小的边并对其进行考察,如果它加入A中不会形成环路,则将其加入A,当A中边数打到|V|-1时,求解完毕。
Q=E A=NULL while(n<|V|-1) e=min(Q) Q=Q-e if(e加入A中不会构成回路) A=A AND e
容易知道一棵最小生成树所包含边的条数为|V|-1。所以求解一棵最小生成树的过程就是选择|V|-1条边的过程。问题的开始相当于所有的节点都是分离的,而问题的结尾要求所有的节点连通。每一条边的加入必须要使图的连通度加一(否则构成环路,则产生冗余的可删除的边)。将问题抽象为:对于一个森林和一些潜在的边,要求加入一条权重最小边使得森林的连通度加一,即森林中树的个数减少一。这样问题就具备了贪婪选择性质:每次应该选择不构成回路的权重最小的边,并且次贪婪选择只引出一个子问题。另外该问题又具备最优子结构,最优解一定包含子问题的最优解,否则可以用子问题的最优解替换当前子问题的解从而得到整个问题的更优解而导出矛盾。因此用贪心策略的Kruskal算法解决最小生成树问题是正确的。
再看Prim算法。Kruskal算法以边的权值作为贪心考虑,每次加入一条能起到作用的权重最小的边,从而得到最优解,是一种很直观的思路。另一种思路,将问题初始阶段看做所有节点都独立,然后构建一个连通的集合,每次按照贪婪的策略向该集合内加入一个点,最终使得所有点都在集合内,便完成了最小生成树的构建。那么应该给每个点赋一个什么样的值来进行贪婪选择呢?应该赋予每个点距离现有集合的距离(与集合里距离最短的点之间的距离)。
伪代码如下:
Q=V A=NULL while(|A|<|V|-1) v=Q中到A中距离最小的点 A=A AND v 由于v的加入,更新Q中剩余点到A的距离
Prim算法在求解的过程中一直维持中途解是一棵树,而Kruskal的中途解则是一个森林。将问题抽象为给定一棵树和一组散节点,通过将散节点中的一个加入树中从而使得树的节点数增一,要求增加的权重最小。这样问题就具有了最优子结构和贪婪选择性质。每步求解后的子问题的最优解一定包含在整个问题的最优解当中,同样也是反证法可以证明;每次的贪婪选择都只会留下一个子问题。这样就证明了Prim算法的正确性。
Dijkstra算法和Prim算法如出一辙。也是每次向一棵树(或者说子图)按照贪心的策略加入一个节点,加入后对会被新加入节点所影响的未加入节点信息进行更新,再重复上述过程直到所有节点都加入为止。区别在于贪心所考虑的指标不同。最小生成树要求的是连通,因此应该将与已构建的部分生成树中的任意节点的距离中最小的一条距离作为贪心考虑,而单源多点最短路径考虑的是所有节点与一个特定原点的距离,因此在进行贪婪选择的时候也应该选择所有未加入的点中与该原点之间距离最短的节点,加入后要更新的信息也是这个,更新方法是考察原有距离和经由刚加入的节点到达原点的距离哪个更小。
伪代码如下:
Q=V A=NULL while(|A|<|V|-1) v=Q中到s距离最小的点 A=A AND v 由于v的加入,更新Q中剩余点到A的距离 for each x in Q dist[x]=min(dist[x],dist[v]+e(u,v))