algorithm-in-python/docs/graph.md

17 KiB
Raw Blame History

title date categories tags keywords mathjax description
图算法 2018-09-06 19:10 数据结构与算法
算法
图,算法 true

1. 图

1.1. 概念

  • 顶点的度 d
  • 相邻
  • 重边
  • 完全图: 所有顶都相邻
  • 二分图: V(G) = X \cup Y, X\cap Y = \varnothing, X中, Y 中任两顶不相邻
  • 轨道

1.1.1. 性质

  • \sum_{v\in V} d(v) = 2|E|
  • G是二分图 \Leftrightarrow G无奇圈
  • 树是无圈连通图
  • 树中, |E| = |V| -1

1.2. 图的表示

  • 邻接矩阵
  • 邻接链表

1.3. 树

无圈连通图, E = V-1, 详细见,

2. 搜索

求图的生成树[^1]

2.1. BFS

for v in V:
    v.d = MAX
    v.pre = None
    v.isFind = False
root. isFind = True
root.d = 0
que = [root]
while que !=[]:
    nd = que.pop(0)
    for v in Adj(nd):
        if not v.isFind :
            v.d = nd.d+1
            v.pre = nd
            v.isFind = True
            que.append(v)

时间复杂度 O(V+E)

2.2. DFS

\Theta(V+E)

def dfs(G):
    time = 0
    for v in V:
        v.pre = None
        v.isFind = False
    for v in V : # note this, 
        if not v.isFind:
            dfsVisit(v)
    def dfsVisit(G,u):
        time =time+1
        u.begin = time
        u.isFind = True
        for v in Adj(u):
            if not v.isFind:
                v.pre = u
                dfsVisit(G,v)
        time +=1
        u.end = time  

begin, end 分别是结点的发现时间与完成时间

2.2.1. DFS 的性质

  • 其生成的前驱子图G_{pre} 形成一个由多棵树构成的森林, 这是因为其与 dfsVisit 的递归调用树相对应
  • 括号化结构
  • 括号化定理: 考察两个结点的发现时间与结束时间的区间 [u,begin,u.end] 与 [v.begin,v.end]
    • 如果两者没有交集, 则两个结点在两个不同的子树上(递归树)
    • 如果 u 的区间包含在 v 的区间, 则 u 是v 的后代

2.3. 拓扑排序

利用 DFS, 结点的完成时间的逆序就是拓扑排序

同一个图可能有不同的拓扑排序

2.4. 强连通分量

在有向图中, 强连通分量中的结点互达 定义 GrevG 中所有边反向后的图

将图分解成强连通分量的算法 在 Grev 上根据 G 中结点的拓扑排序来 dfsVisit, 即

compute Grev
initalization
for v in topo-sort(G.V):
    if not v.isFind: dfsVisit(Grev,v)

然后得到的DFS 森林(也是递归树森林)中每个树就是一个强连通分量

3. 最小生成树

利用了贪心算法,

3.1. Kruskal 算法

总体上, 从最开始 每个结点就是一颗树的森林中(不相交集合, 并查集), 逐渐添加不形成圈的(两个元素不再同一个集合),最小边权的边.

edges=[]
for  edge as u,v in sorted(G.E):
    if find-set(u) != find-set(v):
        edges.append(edge)
        union(u,v)
return edges

如果并查集的实现采用了 按秩合并与路径压缩技巧, 则 find 与 union 的时间接近常数 所以时间复杂度在于排序边, 即 O(ElgE), 而 E\< V^2, 所以 lgE = O(lgV), 时间复杂度为 O(ElgV)

3.2. Prim 算法

用了 BFS, 类似 Dijkstra 算法 从根结点开始 BFS, 一直保持成一颗树

for v in V: 
    v.minAdjEdge = MAX
    v.pre = None
root.minAdjEdge = 0
que = priority-queue (G.V)  # sort by minAdjEdge
while not que.isempty():
    u = que.extractMin()
    for v in Adj(u):
        if v in que and v.minAdjEdge>w(u,v):
            v.pre = u
            v.minAdjEdge = w(u,v)
  • 建堆 O(V) //note it's v, not vlgv
  • 主循环中
    • extractMin: O(VlgV)
    • in 操作 可以另设标志位, 在常数时间完成, 总共 O(E)
    • 设置结点的 minAdjEdge, 需要O(lgv), 循环 E 次,则 总共O(ElgV)

综上, 时间复杂度为O(ElgV) 如果使用的是 斐波那契堆, 则可改进到 O(E+VlgV)

4. 单源最短路

求一个结点到其他结点的最短路径, 可以用 Bellman-Ford算法, 或者 Dijkstra算法.
定义两个结点u,v间的最短路


\delta(u,v) = \begin{cases}
min(w(path)),\quad u\xrightarrow{path} v\\
MAX, \quad u\nrightarrow v
\end{cases}

问题的变体

  • 单目的地最短路问题: 可以将所有边反向转换成求单源最短路问题
  • 单结点对的最短路径
  • 所有结点对最短路路径

4.1. 负权重的边

Dijkstra 算法不能处理负权边, 只能用 Bellman-Ford 算法, 而且如果有负值圈, 则没有最短路, bellman-ford算法也可以检测出来

4.2. 初始化

def initialaize(G,s):
    for v in G.V:
        v.pre = None
        v.distance = MAX
    s.distance = 0

4.3. 松弛操作

def relax(u,v,w):
    if v.distance > u.distance + w:
        v.distance = u.distance + w:
         v.pre = u

性质

  • 三角不等式: \delta(s,v) \leqslant \delta(s,u) + w(u,v)
  • 上界: v.distance \geqslant \delta(s,v)
  • 收敛: 对于某些结点u,v 如果s->...->u->v是图G中的一条最短路径并且在对边进行松弛前任意时间有 u.distance=\delta(s,u)则在之后的所有时间有 v.distance=\delta(s,v)
  • 路径松弛性质: 如果p=v_0 v_1 \ldots v_k是从源结点下v0到结点vk的一条最短路径并且对p中的边所进行松弛的次序为(v_0,v_1),(v_1,v_2), \ldots ,(v_{k-1},v_k), 则 v_k.distance = \delta(s,v_k) 该性质的成立与任何其他的松弛操作无关即使这些松弛操作是与对p上的边所进行的松弛操作穿插进行的。

证明

4.4. 有向无环图的单源最短路问题

def dag-shortest-path(G,s):
    initialize(G,s)
    for u in topo-sort(G.V):
        for v in Adj(v):
            relax(u,v,w(u,v))

4.5. Bellman-Ford 算法

def bellman-ford(G,s):
    initialize(G,s)
    for ct in range(|V|-1): # v-1times
        for u,v as edge in E:
            relax(u,v,w(u,v))
    for u,v as edge in E:
        if v.distance > u.distance + w(u,v):
            return False
    return True

第一个 for 循环就是进行松弛操作, 最后结果已经存储在 结点的distance 和 pre 属性中了, 第二个 for 循环利用三角不等式检查有不有负值圈.

下面是证明该算法的正确性

4.6. Dijkstra 算法

def dijkstra(G,