算法概论笔记 - 图

现实生活中有很大一类问题可以用简洁明了的图论语言来描述,可以转化为图论问题。


图操作问题

相关定义

图可以表示为G=(V, E)

  • 顶点集合V(非空但有限)
  • 连接顶点的边的集合E(可以为空)

通常用|V|表示顶点的数量,用|E|表示边的数量

对于顶点x,y

  • 无向边的表示:{x, y}(对称,无序)
  • 有向边的表示:(x, y) 与(y, x)(有序)

有时边具有权值(weight/ cost)

路径

路径的长 = 该路径上的边数

  • 如果路径不包含边,则路径的长为0,如一个顶点到其自身
  • 环:路径x->x长至少为1
  • 圈:对于有向图,路径x->x长至少为1

无圈的有向图DAG - Directed Acyclic Graph

连通

  • 连通图(无向图):无向图每个顶点到其他每个顶点都存在一条路径
  • 强连通(有向图):有向图每个顶点到其他每个顶点都存在一条路径
  • 弱连通(有向图):有向图的基础图(即其边去掉方向所形成的的图)是连通的
  • 完全图:每一对顶点间都存在一条边的图
  • 连通部件:每个都是原图的一个子图,并且内部顶点间彼此相连通,但却与部件外,即原图中的其他顶点间没有边可达,如下图的{A, B, E, I, J} {C, D, G, H, K, L} {F}
3个连通部件

  • x的入度:以顶点x为终点的边数目
  • x的出度:以顶点x为起点的边数目

图的表示

1. 邻接矩阵

用一维数组存储图中顶点的信息,用二维数组A表示途中各顶点之间的邻接关系或者权值。

对于每条边(x, y),

  • A[x, y] = 1/ weight(如果存在该条边)
  • A[x, y] = 0/ 很大或很小的权值 (如果不存在该条边)

优点:简单,在常量时间内确定某条边是否存在

缺点:空间需求O(|V|^2),适合稠密图(E = O(|V|^2)),然而大部分应用情况并非如此

2. 邻接表(标准方法)

对于每个顶点,使用一个表存放所有邻接的顶点。

优点:空间需求O(|E| + |V|);若边有权,那么附加信息也可以存储在邻接表中。

缺点:确定某条边是否存在的操作需要遍历一个顶点的邻接表

对于非常稀疏的图,当使用ArrayList时程序员可能需要从一个比默认容量更小的容量开始ArrayList,否则可能造成明显的空间浪费。
邻接表的实现:1. 邻接表作为Vetex的映射(Map); 邻接表作为Vetex的数据成员

3. 其他

邻接多重表、十字链表法、边集数组

深度优先搜索

基本问题:给定图中的一个顶点,从该顶点出发,图中那些部分是可达的?

procedure explore(G, v)
Input: G={V, E} is a graph; v in V
Output: visited(v) is set to true for all nodes u reachable from v

    visited(v) = true
    previsit(v)
    for each edge(u, v) in E:
        if not visited(u):explore(u)
    postvisit(v)

procedure dfs(G)
for all v \in V:
    visited(v) = false
for all v \in V:
    if not visited(v):explore(v)

previsit/postvisit 当一个顶点被首次访问/最后一次访问时对其进行的操作

每次在procedure explore中调用procedure explore一次,就能够生成一个连通部件。若仅有一个连通部件,则为连通图

效率:由于对已访问顶点的维护,因此每个顶点仅调用procedure explore一次,在这其中进行了以下步骤:

  1. 一些常量时间的操作 - 标记一个顶点被访问,以及previsit/postvisit操作
  2. 一个对邻边进行遍历的循环,以确定该遍历过程能否将整个搜索过程引向未曾访问的区域

T = T(Step 1) + T(Step 2) = T(|V|) + T(|E|)线性时间

编号

若对顶点访问添加时间信息:

procedure previsit(v)
pre[v] = clock;
clock = clock + 1;

procedure postvisit(v)
post[v] = clock;
clock = clock + 1;

显然,因为栈的特征,对于任意顶点u和v,[pre[u],post[u]]和[pre[v],post[v]]两个区间要么彼此分离,要么一个包含另一个

有向图的深度优先搜索时间信息

对于有向图,我们可以对上图的边进行分类:

  1. 树边:DFS树的实际组成部分;
  2. 前向边:DFS树从一个顶点指向该顶点的一个非子顶点后裔的边;
  3. 回边:DFS树从一个顶点指向其祖先的边;
  4. 横跨边:从一个顶点指向一个已经完全访问过(已进行过postvisit)的顶点。
边(u, v) 边的类型
pre(u)<pre(v)<post(v)<post(u) 树边/前向边
pre(v)<pre(u)<post(u)<post(v) 回边
pre(v)<post(v)<pre(u)<post(u) 横跨边

基于上述定义,可得,有向图含有一个环当且仅当深度优先搜索过程中探测到一条回边

无环性、可线性化以及无回边性实际上是一回事

see also graph.dfs.DeepFirstSearch@judgeBackEdge

迷宫探索(无向图/深度优先搜索)

从一个给定的地点开始迷宫行走,一旦到达某个关节点(相当于顶点),您将面对一些路径(相当于边)供您选择。如果选择不慎,结果可能使您不停地兜圈子,或是使您忽视迷宫中一些可行的路径。

迷宫探索问题 转化-> 可达性问题

关键点:

物品 作用 真实表现
粉笔 标记访问过的顶点 每个顶点维护一个布尔量
细绳 将您带回出发位置 使用栈/或者通过递归隐含地使用栈:延伸以到达一个新顶点;回溯到曾经访问的顶点
方式1

利用粉笔与细绳

see also graph.ExploreMaze

方式2

假设迷宫的起始顶点和出口顶点分别标记为startend,并将无向图视作有向图。那么,我们可以得知路径(u, v)是一条树边/前向边,组成该条路径的也必是若干条树边/前向边。

那么可从pre[]与post[]数组中得出:

  1. pre/post[start/end]判断是否为树边/前向边 - 迷宫是否探索成功
  2. 从start和end两端,夹逼中间的树边/前向边 - 得出中间路径。

特别地,可直接在刚找到end的时间点进行

深度优先实现: TODO

拓扑排序(有向无圈图)

如果存在一条x到y的路径,那么在排序中y就出现在x的后面。

联想: 课程先修结构

方式1

基本思想:

1. find a vertex of zero indegree
2. assign the vertex a topo num
3. decrease the indegree of vertexes which are adjacent to the vertex.
4. jump to step 1 until the topo num runs out
step 1处潜在地查看了所有顶点,可利用队列存储所有入度为0的顶点进行优化。

1. work out the queue containing all the vertexes of zero indegree
2. dequeue the queue to get a vertex, and assign the vertex a topo num
3. decrease the indegree of vertexes which are adjacent to the vertex, `if the indegree decreased to be zero, than add this vertex to the queue`
4. jump to `step 2`

see also graph.TopologySort

无圈通过是否有入度为0的顶点判断

方式2

基本思想:

简单对图顶点执行深度优先搜索,按照顶点的post值降序
进一步,我们可知道,post值最小的顶点一定出现在拓扑排序末尾,一定是汇点 - 出度为0。 对称地,post值最大的一定是源点 - 入度为0。

see also graph.dfs.DfsSolveTopoSort@topoSort

无圈通过回边判断

广度优先搜索(最短路径树)


类似于对树的层序遍历(level-order traversal)

procedure bfs(G, s)
Input: Graph G = {V, E}, s \in V
Output: For all vertexes u reachable form s, dist(u) is set to tht distance from s to u

for all u in V
    dist(u) = \infty

dist(s) = 0
Q = [s](queue containing just s)
while Q is not empty
    u = eject(Q)
    for all edges (u, v) in E
        dist(v) = \infty:
            inject(Q, v)
            dist(v) = dist(u) + 1;

T = 2|V|次队列操作 + 访问边 = O(|V| + |E|)

局限性:边均为单位长度

see also graph.bfs.BreadthFirstSearch

单源最短路径

当前,还不存在找出从s到一个顶点的路径比找出从s到所有顶点路径更快(快得超出一个常数因子)的算法

边为正整数
引入虚顶点

通过在G中的边引入“虚”顶点,分解为单位长度的小段。此时可以通过运行bfs计算最短路径,然而存在效率问题。

对感兴趣的顶点分别设置闹钟(该顶点的预计到达时间)。事实上,随着广度优先搜索的进行,会发现一些捷径,相应顶点的预计到达时间会发生松弛

- s设定的闹钟为时刻0,从s到u的距离是T
- 对于G中u的每个邻居v:
    1. 如果尚未给v设定闹钟,则为其设定闹钟时刻为T+l(u, v)
    2. 如果v的闹钟时刻设定比T+l(u, v)要晚,则将它设定为T+l(u, v)
Dijkstra算法

上述思想即可自然引出Dijkstra算法,该算法可用于解决单源无负边最短路径问题。

数据结构:优先队列,并支持以下操作

  1. 插入新的元素
  2. 减少某个特定元素的键值
  3. 返回并删除键值最小的元素
  4. 用给定的元素以及给定的元素键值构建一个队列

额外数组prev

存储该顶点之前的顶点
通过这些后向指针,从而方便地重建所需的最短路径

procedure dijkstra(G, l, s)
Input: Graph G=(V, E), s \in V)
    positive edge lengths{l_e:e \in E}
Output: For all vertexes u reachable form s, dist(u) is set to tht distance from s to u

for all u \in V
    dist(u) = \infty
    pre(u) = nil

dist(s) = 0
PQ = makequeue(V) (using dist-values as keys)
while PQ is not empty
    u = deletemin(PQ)
    for all edges (u, v) \in E
        if dist(v) > dist(u) + l(u, v)
            dist(v) = dist(u) + l(u, v)
            pre(v) = u
            decreasekey(PQ, v)

Notice: 因初始时pre(u) = nil,因此makequeue时不能抢占该边界次数(即直接将与s相邻的顶点直接加入)。
T = |V|次插入操作 + |V|次deletemin操作 + |E|次decreasekey操作
T严重依赖于优先队列所用的实现方法,例如数组、二分堆、d堆、Fibonacci堆。

若采用数组实现优先队列,则T = O(|V|^2 + E)
see also 数组实现graph.bfs.DijkstraDfsWithArray

若采用二分堆实现优先队列,则T = O((|V|+|E|)log|V|)

堆的高度为log|V|

see also 二分堆实现graph.bfs.DijistraDfsWithHeap
二分堆由最小堆实现,decreasekey操作首先需要找到v在二分堆中的位置。考虑到存在value
相同的元素,则找位置需要耗费O(|V|)复杂度,则将使得T从原来的O((|V|+|E|)log|V|)降为O(|V|log|V|+|V||E|)

  1. 一种好的做法是
    记录(v, dist(v))在堆中的位置,并在优先队列改变时更新

可以考虑使用配对堆(pairing heap)

  1. 另一种方法是在每次dist(v)变化时把(v, dist(v))插入到优先队列中去。这样,在优先队列中的每个顶点可能有多于一个的代表。当deletemin操作把最小的顶点从优先队列中删除时,必须检查以肯定它不是known的。如果它是,则忽略它,并执行另一次deletemin。

因队列的大小可能达到|E|+|V|。由于|E|<=|V|^2, log|E|<=2log|V|。另外,可能需要|E|次而不是|V|次deletemin操作。
T = (|V|+|E|)*log(|E|+|V|) = O((|V|+|E|)log|V|)。时间减慢,时间复杂度不变。

Bellman-Ford算法(可处理负边、可检测负环)

Dijkstra按照正确的顺序更新所有边。对于标记过的点不在进行更新,即使有负权导致最短路径的改变也不会进行重新计算。

而Bellman-Ford算法更新所有边,每条边更新|V|-1次.

考虑到对每条边进行第1次松弛操作的时候,实际上是考虑至多经过(不考虑源点终点)0个点的路径;对每条边进行第2次松弛操作的时候,实际上是考虑至多经过1个点的路径...而对于无负边的图而言,最短路径至多经过n-2个点,因此在|V|-1次迭代后,可以得到最短路径

procedure BellmanFord(G, l, s)
Input: Graph G=(V, E), s \in V
    edge lengths{l_e:e \in E}
Output: For all vertexes u reachable form s, dist(u) is set to tht distance from s to u

for all u \in V
    dist(u) = \infty
    pre(u) = nil

dist(s) = 0
repeat |V|-1 times:
    for all e(u, v) \in E:
        if(dist(v) > dist(u) + e(u,v))
            dist(v) = dist(u) + e(u,v)
            pre(v) = u

可检测负环

  • 在|V|-1次迭代后再执行一次迭代过程。若这最后一次迭代中有某个dist的值被减少,那么图中存在一个负环。
  • 图中出现负环,最短路径问题本身就没有意义

T = O(|V||E|)
优化

  • 若在该轮,所有的边都没有relax的话,算法便可结束。因此有时候无需经过|V|-1次迭代
  • 每次仅对最短路径估计值发生变化了的顶点的所有出边执行松弛操作

可用于解m个n元约束组,即相当于n个顶点m条边。

复杂度为O(m*n)

TODO 差分约束系统和SPAF

基于拓扑排序的最短路径(有向无圈图)

通过深度优先搜索线性化有向无环图,然后按照得到的顶点顺序访问顶点,对从当前访问顶点出发的边执行更新操作。

procedure dag-shortest-paths(G, l, s)
Input: Graph G=(V, E), s \in V
    edge lengths{l_e:e \in E}
Output: For all vertexes u reachable form s, dist(u) is set to tht distance from s to u

for all u \in V
    dist(u) = \infty
    pre(u) = nil

dist(s) = 0
Linearize G
for each u \in V in linearized order:
    for all edges (u, v) \in E:
        if(dist(v) > dist(u) + e(u,v))
            dist(v) = dist(u) + e(u,v)
            pre(v) = u

当一个顶点v被选取以后,按照拓扑排序的法则它没有从unknown顶点发出的进入边,因此它的距离dist(v)不再被降低。

T = O(|V| + |E|)

see also graph.TopoSortSolveShortPath

单源最长路径(有向无圈图)

对于一般的图,最长路径问题通常没有意义,因为可能有正值圈(等价于最短路径问题中的负值圈)存在。对于DAG而言,将每条边的长度设为其负值,使用前述的拓扑排序方法,即可得到单源最长路径。

see also graph.TopoSortSolveLongPath

应用

事件节点图

  • 最早完成时间:从节点1开始,按照拓扑顺序,计算单源最长路径
  • 最晚完成时间:从节点n开始,按照反序的拓扑顺序,计算单源最短路径

网络流问题

给定有向图G=(V, E),其边容量为e(u, v)。

容量代表一个管道允许通过的水的最大流量。

有两个顶点,发点s和收点t,最大流问题就是确定从s到t可以通过的最大流量。

必须满足流守恒:进入的都必然流出

方法:构造构造流图(算法终止时包含最大流)、残余图(表示对于每条边的剩余流容量)。寻找增长通路,构造反向弧。当没有增长通路时,算法终止。

不需要保证是无圈图

增长通路:残余图中从s到t的一条路径。该路径上的最小边值就是可以添加到路径每一边上的流的量。

反向弧:对于流图中具有流f(u, v)的每一边(u, v),我们在残余图中添加一条容量为f(u, v)的反向边(v, u)。

使算法可以解除/部分解除它原来的决定,使得可以找到最优解

通过bfs寻找增广路径,T = O(|E|^2*|V|)
see also graph.bfs.NetWorkFlowEk
see more

参考文献

1. bellman-ford算法与差分约束系统

写在最后

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

推荐阅读更多精彩内容

  • https://zh.visualgo.net/graphds 浅谈图形结构https://zh.visualgo...
    狼之独步阅读 3,980评论 0 0
  • 第一章 绪论 什么是数据结构? 数据结构的定义:数据结构是相互之间存在一种或多种特定关系的数据元素的集合。 第二章...
    SeanCheney阅读 5,660评论 0 19
  • 图是一种比线性表和树更复杂的数据结构,在图中,结点之间的关系是任意的,任意两个数据元素之间都可能相关。图是一种多对...
    Alent阅读 2,236评论 1 22
  • -DFS(Depth First Search):深度优先搜索 访问完一个顶点的所有邻接点之后,会按原路返回,对应...
    Spicy_Crayfish阅读 2,774评论 1 0
  • 第二天我拖着病躯,和万熙然一起将出租屋里的生活用品转移到了陈梵给我的房子里。屋子一看就是小可爱连夜布置了一遍的,窗...
    和这个世界温柔的相处阅读 245评论 1 6