算分-GRAPH ALGORITHMS
Graph Search:
图的种类:最常用的是简单无向图,而且假设顶点始终是有穷多个。复杂图是指有环和平行边的图。我们称两个顶点是相邻的(不妨设u,v),如果(u,v)之间有一条边。
图的表示:有两种最流行的表示方式,第一种的用n^2的对称矩阵(元素为0或1)来表示,如果是0的话,代表顶点i,j之间没有边连接;如果是1的话,代表顶点i,j之间有边连接,一个不好的地方是空间代价比较大。第二种表示方式是考虑到一个事实,也就是多顶点的图常常为稀疏的图,即边的数量不会比顶点数大很多,此时用邻接表的数据结构来存储。
DFS搜索:由于图一般无序,通常可以有多种访问的方法。但是为了避免重复访问,我们需要对每个顶点是否访问过做一个标记,可以用一个数组来存储,也可以在顶点的数据结构中加入这个标志。伪代码如下:
1 void visit(int i) 2 time++; V[i].d = time 3 for all outgoing edges ij do: 4 if V[j].d = 0 then 5 V[j].p = i; visit(j) 6 end if 7 end for; 8 time++;V[i].f = time. 9 10 time = 0 11 for all vertice i do V[i].d = 0 end for; 12 for all vertice i do 13 if V[i].d = 0 then 14 V[i].p = 0; visit(i) 15 end if 16 end for.
不难发现这个算法复杂度是O(n+m)的,其中n为顶点个数, m为总边数。而且这样的算法会让我们得到每个结点的开始访问时间d和结束访问时间f,对于两个节点i和j,j的访问顺序在i的后面当且仅当v[j].d > v[i].d && v[j].f < v[i].f.接着介绍一个有向非周期(循环)图,一个有向图是非周期(循环的)的,当且仅当它是一个linear extension的,即线性拓展的(留给大家证明)。最后介绍了bfs和dfs做拓扑排序的问题,不再介绍。
Shortest Paths:
第一种是借助bfs来解决边长为等长的情况,复杂度是O(n+m)。第二种是边长带权重的情况,此时用迪杰斯特拉算法解决问题,算法详情及其正确性的证明我不再介绍,见下图:
1 while priority queue is not-empty do 2 i = ExtractMIN; mark i 3 for all neighbors j of i do 4 if j is unmarked then 5 V[j].d = min(V[j].d, V[i].d + weight(i,j)) 6 end if 7 end for 8 end while
最多执行n次extractmin操作,最后执行m次decrease key操作,根据斐波那契堆的性质,我们可以把复杂度降到O(nlogn + m)d的复杂度。
另一个问题是解决一个源点到其他所有点之间的最短路径问题,可以两两使用迪杰斯特拉算法,但是要n次,复杂度为O(n^2logn + nm),第二种是使用floyd算法,复杂度为O(n^3),代码如下:
1 for k = 1 to n do 2 for i = 1 to n do 3 for j = 1 to n do 4 A[i,j] = min{A[i,j]], A[i,k] + A[k,j]} 5 end for 6 end for 7 end for
Minimum Spanning Trees:
MST是最小生成树,prim算法+kruskal算法,不再介绍。
Union find:
我们在实现kruskal算法中要用到Union find的操作,这个问题的有趣之处在于m个元素的复杂度只是比线性的m稍微大一点点。抽象数据类型ADT如下:
set FIND(i):return P ∈ C with i = P;
void UNION(P,Q): C = C - {P,Q} U {P U Q}.
在很多情况下,集合本身是不相关的,但是知道两个元素之间的关系才是最重要的,比如在Kruskal算法中要用到的几步如下:
1 P = FIND(i) 2 Q = FIND(j) 3 if P != Q then UNION(P,Q) end if;
第一种实现的方法是用一些数组,存储每个元素的所属的set,对应set的代表元素存集合的set,以及每个元素的next(代表元素为0)。UNION方法实现如下:(同时可以证明n-1次union操作的代价是O(NlogN)的。
1 void UNION(int P, Q) 2 if C[P].size < C[Q].size then P ↔ Q endif; 3 C[P].size = C[P].size + C[Q].size; 4 second = C[P].next; C[P].next = Q; t = Q; 5 while t != 0 do 6 C[t].set = P; u = t; t = C[t].next 7 endwhile; C[u].next = second
第二种是用树来实现,之前讲过,同时也涉及按权重合并+路径压缩的方法来优化问题。可以得到,m次find操作的复杂度为O(mlog*n)其中log*n可以被优化为ACKERMAN函数,比log*n增长得更加慢。
Homework: