图算法

image.png

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)
如果使用的是 斐波那契堆, 在 设置 minAdjEdge时 调用 decrease-key, 这个操作摊还代价为 O(1), 所以时间复杂度可改进到 O(E+VlgV)

4. 单源最短路

求一个结点到其他结点的最短路径, 可以用 Bellman-ford算法, 或者 Dijkstra算法.
定义两个结点u,v间的最短路
\delta(u,v) = \begin{cases} \min(w(path)),\quad u\xrightarrow{path} v\\ \infty, \quad u\nrightarrow v \end{cases}
问题的变体

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

最短路的子路径也是最短路径

p=(v_0,v_1,\ldots,v_k)为从结点v_0v_k的一条最短路径, 对于任意0\le i\le j \le k, 记p_{ij}=(v_i,v_{i+1},\ldots,v_j)为 p 中 v_iv_j的子路径, 则 p_{ij}v_iv_j的一条最短路径

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. 有向无环图的单源最短路问题

\Theta(V+E)

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 算法

O(VE)

def bellman-ford(G,s):
    initialize(G,s)
    for ct in range(|V|-1): # v-1 times
        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 算法

O(ElogV), 要求不能有负值边

Dijkstra算法既类似于广度优先搜索(,也有点类似于计算最小生成树的Prim算法。它与广度优先搜索的类似点在于集合S对应的是广度优先搜索中的黑色结点集合:正如集合S中的结点的最短路径权重已经计算出来一样,在广度优先搜索中,黑色结点的正确的广度优先距离也已经计算出来。Dijkstra算法像Prim算法的地方是,两个算法都使用最小优先队列来寻找给定集合(Dijkstra算法中的S集合与Prim算法中逐步增长的树)之外的“最轻”结点,将该结点加入到集合里,并对位于集合外面的结点的权重进行相应调整。

def dijkstra(G,s):
    initialize(G,s)
    paths=[]
    q = priority-queue(G.V) # sort by distance
    while not q.empty():
        u = q.extract-min()
        paths.append(u)
        for v in Adj(u):
            relax(u,v,w(u,v))

5. 所有结点对的最短路问题

5.1. 矩阵乘法

使用动态规划算法, 可以得到最短路径的结构
l_{ij}^{(m)}为从结点i 到结点 j 的至多包含 m 条边的任意路径的最小权重,当m = 0, 此时i=j, 则 为0,
可以得到递归定义
l_{ij}^{(m)} =\min( l_{ij}^{(m-1)}, \min_{1\leqslant k\leqslant n}( l_{ik}^{(m-1)}+w_{kj})) = \min_{1\leqslant k\leqslant n}( l_{ik}^{(m-1)}+w_{kj}))
由于对于所有 j, 有 w_{jj}=0,所以上式后面的等式成立.

由于是简单路径, 则包含的边最多为 |V|-1 条, 所以
\delta(i,j) = l_{ij}^{(|V|-1)} = l_{ij}^{(|V|)} =l_{ij}^{(|V| + 1)}= ...
所以可以从自底向上计算, 如下
输入权值矩阵 W(w_{ij})), L^{(m-1)},输出L^{(m)}, 其中 L^{(1)} = W,

def  f(L, W):
  n = L.rows
  L_new = new matrix(row=n ,col = n)
  for i in range(n):
      for j in range(n):
          L_new[i][j] = MAX
          for k in range(n):
              L_new[i][j] = min(L_new[i][j], L[i][k]+w[k][j])
  return L_new

可以看出该算法与矩阵乘法的关系
L^{(m)} = W^m,
所以可以直接计算乘法, 每次计算一个乘积是 O(V^3), 计算 V 次, 所以总体 O(V^4), 使用矩阵快速幂可以将时间复杂度降低为O(V^3lgV)

def f(W):
    L = W
    i = 1
    while i<W.rows:
        L = L*L
        i*=2
    return L

5.2. Floyd-Warshall 算法

同样要求可以存在负权边, 但不能有负值圈. 用动态规划算法:
d_{ij}^{(k)} 为 从 i 到 j 所有中间结点来自集合 {\{1,2,\ldots,k\}} 的一条最短路径的权重. 则有
d_{ij}^{(k)} = \begin{cases} w_{ij},\quad k=0\\ min(d_{ij}^{(k-1)},d_{ik}^{(k-1)}+d_{kj}^{(k-1)}),\quad k\geqslant 1 \end{cases}
而且为了找出路径, 需要记录前驱结点, 定义如下前驱矩阵 \Pi, 设 \pi_{ij}^{(k)} 为 从 i 到 j 所有中间结点来自集合 {\{1,2,\ldots,k\}} 的最短路径上 j 的前驱结点

\pi_{ij}^{(0)} = \begin{cases} nil,\quad i=j \ or \ w_{ij}=\infty \\ i, \quad i\neq j\ and \ w_{ij}<\infty \end{cases}
k\geqslant 1
\pi_{ij}^{(k)} = \begin{cases} \pi_{ij}^{(k-1)} ,\quad d_{ij}^{(k-1)}\leqslant d_{ik}^{(k-1)}+d_{kj}^{(k-1)}\\ \pi_{kj}^{(k-1)} ,\quad otherwise \end{cases}

由此得出此算法

def floyd-warshall(W):
    n = len(W)
    D= W
    initialize pre
    for k in range(n):
        pre2 = pre.copy()
        for i in range(n):
            for j in range(n)
                if d[i][j] > d[i][k]+d[k][j]:
                    d[i][j] =d[i][k]+d[k][j]
                    pre2[i][j] = pre[k][j]
        pre = pre2
return d,pre

5.3. Johnson 算法

思路是通过重新赋予权重, 将图中负权边转换为正权,然后就可以用 dijkstra 算法(要求是正值边)来计算一个结点到其他所有结点的, 然后对所有结点用dijkstra

  1. 首先构造一个新图 G'
    先将G拷贝到G', 再添加一个新结点 s, 添加 G.V条边, s 到G中顶点的, 权赋值为 0
  2. 用 Bellman-Ford 算法检查是否有负值圈, 如果没有, 同时求出 \delta(s,v) 记为 h(v)
  3. 求新的非负值权, w'(u,v) = w(u,v)+h(u)-h(v)
  4. 对所有结点在 新的权矩阵w'上 用 Dijkstra 算法


    image.png
JOHNSON (G, u) 

s = newNode
G' = G.copy()
G'.addNode(s)
for v in G.V: G'.addArc(s,v,w=0)

if BELLMAN-FORD(G' , w, s) ==FALSE 
    error "the input graph contains a negative-weight cycle" 

for v in G'.V:
    # computed by the bellman-ford algorithm, delta(s,v) is the shortest distance from s to v
    h(v) = delta(s,v) 
for edge(u,v) in G'.E:
    w' = w(u,v)+h(u)-h(v)
d = matrix(n,n)
for u in G:
    dijkstra(G,w',u) # compute delta' for all v in G.V
    for v in G.V:
        d[u][v] = delta'(u,v) + h(v)-h(u)
return d

6. 最大流

G 是弱连通严格有向加权图, s为源, t 为汇, 每条边e容量 c(e), 由此定义了网络N(G,s,t,c(e)),

  • 流函数 f(e):E \rightarrow R
    \begin{aligned} (1)\quad & 0\leqslant f(e) \leqslant c(e),\quad e \in E\\ (2)\quad & \sum_{e\in \alpha(v)} f(e)= \sum_{e\in \beta(v)}f(e),\quad v \in V-\{s,t\} \end{aligned}
    其中 \alpha(v) 是以 v 为头的边集, \beta(v)是以 v 为尾的边集
  • 流量: F = \sum_{e\in \alpha(t)} f(e)- \sum_{e\in -\beta(t)}f(e),
  • (S,\overline S): S\subset V,s\in S, t\in \overline S =V-S
  • 截量C(S) = \sum_{e\in(S,\overline S)}c(e)

6.1. 定理[2]

  • 对于任一截(S,\overline S), 有 F = \sum_{e\in (S,\overline S)} f(e)- \sum_{e\in(\overline S,S)}f(e),
    prove
  • F\leqslant C(S)
    证明: 由上面定理
    F = \sum_{e\in (S,\overline S)} f(e)- \sum_{e\in(\overline S,S)}f(e),
    0\leqslant f(e) \leqslant c(e), 则
    F\leqslant \sum_{e\in (S,\overline S)} f(e) \leqslant \sum_{e\in (S,\overline S)} c(e) = C(S)
  • 最大流,最小截: 若F= C(S), 则F'是最大流量, C(S) 是最小截量

6.2. 多个源,汇

可以新增一个总的源,一个总的汇,


6.3. Ford-Fulkerson 方法

由于其实现可以有不同的运行时间, 所以称其为方法, 而不是算法.
思路是 循环增加流的值, 在一个关联的"残存网络" 中寻找一条"增广路径", 然后对这些边进行修改流量. 重复直至残存网络上不再存在增高路径为止.

def ford-fulkerson(G,s,t):
    initialize flow f to 0
    while exists an augmenting path p in residual network Gf:
        augment flow f along p
    return f

6.3.1. 残存网络

6.3.2. 增广路径

6.3.3. 割

6.4. 基本的 Ford-Fulkerson算法

def ford-fulkerson(G,s,t):
    for edge in G.E: edge.f = 0
    while exists path p:s->t  in Gf:
        cf(p) = min{cf(u,v):(u,v) is in p}
        for edge in p:
            if edge  in E:
                edge.f +=cf(p)
            else: reverse_edge.f -=cf(p)

6.5. TBD

7. 参考资料


  1. 算法导论

  2. 图论, 王树禾

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容