一. 拓扑排序:
1.基于入度的拓扑排序(Kahn's Algorithm)
(1) 算法思想:
Step1:计算所有结点的入度(遍历所有的结点,将其相邻结点的入度+1,时间复杂度为);
Step2:将所有入度为0的点加入到一个队列;
Step3:取出队头元素,然后pop();
(1)count变量表示访问过的结点个数
(2)top数组将每次出队的元素保存
(3)将这个点的所有相邻的点的入度-1;如果每个相邻的结点的入度减少为0后,加入队列中;
Step4:重复步骤3直到队列为空。
(2)伪代码:
1 while(!q.empty()){
2 V v=q.front;
3 top.push(v);
4 q.pop();
5 count++;
6 for(itr=adj[v].begin();itr!=adj[v].end();itr++){
7 if(--indegree[itr]==0)
8 q.push(itr);
9 }
10 }
(3)应用:判断一个有向图是否有环
Step5:如果count和顶点个数相同,则将top数组中元素按序输出就是排序的结果,如果count不等于顶点个数,(不等情况下,一般是count>顶点个数),说明图中存在环,拓扑排序无法完成。
2.基于DFS(出度)的拓扑排序
(1)算法思想:
Step1: 调用DFS(G);
Step2: 记录结点v的完成时间v.f,将v.f存放在拓扑排序顺序数组topoSort[]中;
Step3: 拓扑排序得到的线性序列,即为topoSort[]的逆序;
(2)伪代码:
二.强连通分量:
1.Kosaraju算法
(1)算法思想:
Step1: 调用DFS(G),即”伪拓扑排序“;
Step2: 对G进行转置得到GT;
Step3:按照Step1中结点出栈的顺序,调用DFS(GT),得到若干搜索树,每个搜树就是一个强连通分量;
三.最小生成树
1.Prim算法
(1)算法思想:
Step1:图的所有顶点集合为V;初始令集合u={s},v=V−u;;
Step2:u,v组成的边中,选择代价最小的边(u0,v0),加入到最小生成树中,并把v0并入到集合u中;
Step3:重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止;
(2)伪代码:
MST-PRIM(G,W,r):
1 for each u in G.V
2 u.key=+∞;
3 u.π=NULL;
4 r.key=0;
5 Q=G.V;
6 while Q!=NULL
7 u=EXTRACT-MIN(Q);
8 for each v in G.Adj[u];
9 if (v in Q) and w(u,v)<v.key
10 v.π=u;
11 v.key=w(u,v);
2.Kruskal算法
(1)算法思想:
Step1: 把图中的所有边按代价从小到大排序;
Step2: 把图中的n个顶点看成独立的n棵树组成的森林;
Step3: 按权值从小到大选择边,所选的边连接的两个顶点ui,vi,应属于两颗不同的树,则成为最小生成树的一
条边,并将这两颗树合并作为一颗树;
Step4: 重复Step3直到所有顶点都在一颗树内或者有n-1条边为止
(2)伪代码:
MST-KRUSKAL(G,w):
1 A=NULL;
2 for each vertex v in G.V
3 MAKE-SET(v);
4 sort the edges of G.E into nondecreasing order by weight w
5 for each adges(u,v) in G.E,take in nondecreasing order by weight
6 if FIND-SET(v)!=FIND-SET(v)
7 A=A+{(u,v)}
8 UNION(u,v);
9 retrun A
四.单源最短路径
1.Bellman-Ford算法
(1)算法思想:
Step1: 初始化所有点。每一个点保存一个值,表示从原点到达这个点的距离,将原点的值设为0,其它的点的
值设为无穷大(表示不可达);
Step2: 进行循环,循环下标为从1到n-1(n等于图中点的个数),在循环内部,遍历所有的边,进行松弛计算;
Step3: 遍历途中所有的边(edge(u,v)),判断是否存在这样情况:d(v)> d (u) + w(u,v),若存在则返回false,
表示途中存在从源点可达的权为负的回路;
(2)伪代码:
BELLMAN-FORD(G, w, s)
1 INITIALIZE-SINGLE-SOURCE(G, s)
2 for i ← 1 to |V[G]| - 1
3 do for each edge (u, v) ∈ E[G]
4 do RELAX(u, v, w)
5 for each edge (u, v) ∈ E[G]
6 do if d[v] > d[u] + w(u, v)
7 then return FALSE
8 return TRUE
2.Dijkstra算法
(1)算法思想:
Step1: 初始时,S只包含源点,即S={v},v的距离为0。U包含除v外的其他顶点,即:U={其余顶点},若v
与U中顶点u有边,则<u,v>正常有权值,若u不是v的出边邻接点,则<u,v>权值为∞;
Step2: 从U中选取一个距离v最小的顶点k,把k,加入S中(该选定的距离就是v到k的最短路径长度);
Step3: 以k为新考虑的中间点,修改U中各顶点的距离;若从源点v到顶点u的距离(经过顶点k)比原来距
离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值的顶点k的距离加上边上的权
Step4:重复步骤Step2和Step3直到所有顶点都包含在S中.
(2)伪代码:
DIJKSTRA(G,w,s):
1 INITIALIZE-SINGLE-SOURCE(G,s)
2 S=∅
3 Q=G.V
4 while Q!=∅
5 u=EXTRACT-MIN(Q)
6 S=S∪{u}
7 for each vertex v∈G.Adj[u]
8 RELAX(u,v,w)
3.有向无环图中的单源最短路径问题
(1)算法思想:
根据结点的拓扑排序次序来对带权重的有向无环图G进行边的松弛操作,时间复杂度O(V+E);
(2)伪代码:
DAG-SHORTEST-PATH(G,w,s):
1 topologically sort the vertices of G
2 INITIALIZE-SINGLE-SOURCE(G,S)
3 for each vertex u,taken in topologically sorted order
4 for each vertex v∈G.Adj[u]
5 RELAX(u,v,w)
中间过程:
松弛操作:
RELAX(u,v,w)
1 if v.d>u.d+w(u,v)
2 v.d=u.d+w(u,v);
3 v.π=u;
初始化操作:
INITIALIZE-SINGLE-SOURCE(G,s)
1 for each vertex v ∈G.V
2 v.d=无穷大;
3 v.π=null;
4 s.d=0;
(注:v.π表示前驱结点,v.d用于记录从源结点s到结点v的最短路径权重的上界,
松弛过程为:测试是否可以对从s到v的最短路径进行改善)
二. 比较分析
1.拓扑排序两种算法比较
(1)分别使用队列和栈存储;
(2)基于DFS的算法,加入结果集的条件是:结点的出度为0;
(3)Kahn算法,加入结果集的条件是:结点的入度为0;
(4)一个有向图G=(V,E)是无环的当且仅当对其进行的深度优先搜索不产生后向边;
2.强连通分量
(1)算法时间复杂度:两次深度优先搜索计算图G的强连通分量,其中一次运行在G上,另一次
运行在GT上,所以复杂度为;
3.最小生成树
(1)两种算法都是基于贪心策略,每步通过寻找局部最优(安全边)来获得全局最优解;
(2)需要维持一个最小优先队列,使用普通的二叉堆,时间复杂度限制在,
Prim算法使用斐波那契堆则时间复杂度为;
4.单源最短路径
(1)Dijkstra算法和用于有向无环图的最短路径算法对每条边仅松弛操作一次,
而Bellman-Ford算法对每条边松弛操作|V|-1次;
(2)时间复杂度:Dijkstra算法时间复杂度同样依赖最小队列的实现,使用二叉堆来实现最小优先
队列,则算法运行总时间为,使用斐波那契堆为;
5.相关定理
定理1.拓扑排序算法的正确性:
对于任意一对不同的结点u,v∈V,如果G中包含一条从结点u到结点v的边,则v,f<u,f;
定理2.强连通分量算法的正确性:递归证明
定理3.最短路径的子路径也是最短路径;
定理4.Bellman-Ford算法正确性证明:
定理5.Dijkstra算法正确性证明: