201905图论总结——又来一坑(并上csp初赛前图论复习)

图论的话,其实就是那么几种算法,本蒟蒻学得也不多。
所以就学过的来个总结…

一、最短路

图分为有向图和无向图,一般用邻接表和邻接矩阵来存。无向图其实可以把其中的无向边作为两条方向相反的有向边,存储方法也就和有向图一样了。
下面为几种题型:

1、单源最短路径

一般题型为给定一张有向图,有 n n n 个点, m m m 条边,用( x x x, y y y, z z z)表示从 x x x 出发,到达 y y y,长度为 z z z 的有向边。一般以1号点为起点,求 d [ i ] d[i] d[i] 1 < = i < = n 1<=i<=n 1<=i<=n),表示从起点 1 1 1 到节点 i i i 的最短路经长。算法一般有以下几种

Dijkstra算法

简称DJ算法,这是一种基于贪心思想,只适用于所有边没有负数的算法。
流程:
一般先初始化 d [ 1 ] = 0 d[1]=0 d[1]=0,其余的 d d d 值为无穷大并标为 v [ 1 ] = 1 v[1]=1 v[1]=1,其余的 v v v 标为 0 0 0
找出一个未标记过的且 d d d 值最小的节点 x x x,然后标记节点 x x x
扫描节点 x x x 的所有出边( x x x, y y y, z z z),若 d [ y ] > d [ x ] + z d[y]>d[x]+z d[y]>d[x]+z,就用 d [ x ] + z d[x]+z d[x]+z 更新 d [ y ] d[y] d[y]
重复2、3两个步骤直至所有点都被标记。

Bellman-Ford算法和SPFA算法

Bellman-Ford算法是基于迭代思想的。
流程:
扫描所有边( x x x, y y y, z z z),若 d [ y ] > d [ x ] + z d[y]>d[x]+z d[y]>d[x]+z 则用 d [ x ] + z d[x]+z d[x]+z 更新 d [ y ] d[y] d[y]
重复上步骤直至没有更新操作发生。
时间复杂度为 O ( n m ) O(nm) Onm
而SPFA算法也被叫做“队列优化的Bellman-Ford算法”,可以说是用了广搜变形。
流程:
建立一个队列,最初队列中只含有起点 1 1 1
取出队头节点 x x x,扫描他所有的边( x x x, y y y, z z z),若 d [ y ] > d [ x ] + z d[y]>d[x]+z d[y]>d[x]+z 则用 d [ x ] + z d[x]+z d[x]+z 更新 d [ y ] d[y] d[y],同时,若 y y y 不在队列中,则让 y y y 入队(是否入队需要另开一个数组标记);
重复上步骤直至队列为空。
这个算法避免了对不需要扩展的节点冗余扫描,时间复杂度一般为 O ( k m ) O(km) O(km) k k k 为一个较小的常数),但在特殊的图也要小心容易会退化为 O ( n m ) O(nm) O(nm)的算法,望慎重。
注意到,DJ不能有负数的边,但这两种算法可以有。如果没负数,还可以用STL中的二叉堆优化SPFA算法。

2、任意两点间最短路径
Floyd算法

d [ k , i , j ] d[k,i,j] d[k,i,j] 表示经过编号不超过 k k k 的节点从 i i i j j j 的最短路径长度。用分治的思想,可以将这个问题分成两个子问题:经过编号不超过 k − 1 k-1 k1 的节点从 i i i j j j 的最短路径长度和从 i i i j j j 的路径长度中较小的那个,所以状态转移方程为: d [ k , i , j ] = m i n ( d [ k − 1 , i , j ] , d [ k − 1 , i , k ] + d [ k − 1 , k , j ] ) d[k,i,j]=min(d[k-1,i,j],d[k-1,i,k]+d[k-1,k,j]) d[k,i,j]=min(d[k1,i,j],d[k1,i,k]+d[k1,k,j]),初值 d [ 0 , i , j ] = g [ i , j ] d[0,i,j]=g[i,j] d[0,i,j]=g[i,j] g [ i , j ] g[i,j] g[i,j] 为读入的邻接矩阵)。
显然发现这种算法的本质是动态规划, k k k 是它的阶段,所以必须放在最外层(这点是需要尤其注意的)。

传递闭包

在交际网络中,给定若干个元素和若干对二元关系,且具有传递性。通过传递性推到出尽量多的元素之间的关系和问题被称作传递闭包。这使距离就变成了两点之间是否连通,这种题目可用Floyd算法解决。

二、最小生成树

给定一张边带权的无向图 G = ( V , E ) , n = ∣ V ∣ , m = ∣ E ∣ G=(V,E),n=|V|,m=|E| G=(V,E),n=V,m=E。由 V V V 中全部 n n n 个顶点和 E E E n − 1 n-1 n1 条边构成的无向连通子图称为 G G G 的一棵生成树。边的权值之和最小的生成树称作 G G G 的最小生成树(MST)。
定理:任意一棵最小生成树一定包含无向图中权值最小的边,可以用反证法证明。
推论:给定一张无向图 G = ( V , E ) , n = ∣ V ∣ , m = ∣ E ∣ G=(V,E),n=|V|,m=|E| G=(V,E),n=V,m=E。从 E E E 中挑出 k < n − 1 kk<n1 条边构成 G G G 的一个生成森林。若再从剩余的 m − k m-k mk 条边中选 n − 1 − k n-1-k n1k 条添加到生成森林中,使其成为 G G G 的生成树并且选出的边的边权值和最小,则生成树一定包含这 m − k m-k mk 条边中链接成森林的两个不连接节点的权值最小的边。

Kruscal算法

由上面的推论引出此算法,每次操作从剩余的边中选出一条权值最小的把森林中的两颗树相连(可用并查集维护)。
流程:
建立并查集,把所有边以权值大小排序,每次枚举最小的,若两短点在同一并查集中,搜索下一条边,若不在,合并两端点所在的并查集,把边权值加到答案里之后,搜索下一条,直至所有边都被搜索过。

Prim算法

也是从上面的推论引出的算法,但思路略微有些不同,这个算法总是维护最小生成数的一部分,假设我们已经确定属于最小生成树的节点集合 T T T,剩余节点集合 S S S,找到 m i n x ∈ T , y ∈ S ( z ) min_{x\in T,y\in S} (z) minxT,yS(z) z z z x , y x,y x,y 连接的边的权值),然后把 y y y S S S 中删除,加到 T T T 中并把答案累加上 z z z,再类比DJ算法用一个数组标记节点属于哪个集合,然后每次操作都更新一个数组来求最短边。

三、树的直径与最近公共祖先

1、树的直径

一棵树中距离最远的两个点之间的的距离称之为数的直径(最长连),在接下来的两种方法中,时间复杂度均为 O ( n ) O(n) O(n) ,假设 n n n 个点的 n − 1 n-1 n1 条边的无向图形式已存在领接表中。

树形dp求树的直径

1 1 1 号点为根,相当于就把无向图转化成了一棵有根树。
d [ x ] d[x] d[x] 来表示从 x x x 出发走向以 x x x 为根的子树,能够到达的最远节点的距离。设 x x x 的子节点为 y 1 , y 2 , y 3 . . . y k y_1,y_2,y_3...y_k y1,y2,y3...yk e ( x , y ) e(x,y) e(x,y) 表示 x , y x,y x,y的边权,显然 d [ x ] = m a x 1 < = i < = k ( d [ y i ] + e ( x , y i ) ) d[x]=max_{1<=i<=k}(d[y_i]+e(x,y_i)) d[x]=max1<=i<=k(d[yi]+e(x,yi))。接下来再考虑每个节点 x x x,求出经过 x x x 的最长链的长度 f [ x ] f[x] f[x],那么整棵树的直径就是这些 f [ x ] f[x] f[x] 中的最大值。
如何求 f [ x ] f[x] f[x] ?它由四个部分构成: y i y_i yi y i y_i yi 子树中的最远距离,边 ( x , y i ) (x,y_i) (x,yi),边 ( x , y j ) (x,y_j) (x,yj) y j y_j yj y j y_j yj 子树中的最远距离,我们可以设 i < j ii<j,于是有: f [ x ] = m a x 1 < = i < j < = k ( d [ y i ] + d [ y j ] + e ( x , y i ) + ( x , y j ) ) f[x]=max_{1<=if[x]=max1<=i<j<=k(d[yi]+d[yj]+e(x,yi)+(x,yj))
,枚举时只要枚举当前点的子节点用两者的最长链相加再加上两者的连边,注意小心重复。

两次bfs求树的直径

通过两次bfs或dfs也可以求树的直径,只要从任意节点出发,用bfs或dfs对树进行一次遍历,求出与出发点距离最远的节点记为 p p p,然后再来一遍求出距离 p p p 最远的点 q q q,从 p p p q q q 的路径即为树的一条直径,因为你开始第一遍做的时候,没有更长的链了,说明 p p p 必定在直径的一端。

2、最近公共祖先(LCA)

给定一棵有根树,若节点 z z z 既是节点 x x x 的祖先,又是 y y y 的祖先,那么称 z z z x , y x,y x,y 的公共祖先。在它们所有公共祖先中,深度最大的就是 x , y x,y x,y 的最近公共祖先,记为 l c a ( x , y ) lca(x,y) lca(x,y)
接下来介绍几种算法。

向上标记法

x x x 向上走到根节点,并标记所有经过的节点,再从 y y y 向上走到根节点,当第一次遇到已标记的点时,这个点就是 l c a ( x , y ) lca(x,y) lca(x,y)
时间复杂度最坏为 O ( n ) O(n) O(n)

树上倍增法

这个算法很重要,不光可以应用在求LCA上呢。但这里主题还是讲讲在求LCA上怎么用吧。
f ( x , k ) f(x,k) f(x,k) 表示 x x x 2 k 2^k 2k 辈的祖先,即从 x x x 向根节点走了 2 k 2^k 2k 步达到的节点。如果这个节点不存在,那么就标记为0。
由上得出, f [ x , 0 ] f[x,0] f[x,0] 即是 x x x 的父节点,此外 f [ x . k ] = f [ f [ x , k − 1 ] , k − 1 ] ( k ∈ ( 1 , l o g ( n ) ) ) f[x.k]=f[f[x,k-1],k-1](k\in (1,log(n))) f[x.k]=f[f[x,k1],k1](k(1,log(n)))
这种方法的阶段就是当前节点的深度,所以用bfs在每个节点入队前计算 f f f 数组的值,时间复杂度为 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n)),预处理完之后,每次可用 O ( n ) O(n) O(n) 的时间复杂度询问 x , y x,y x,y 的LCA。
流程:
d [ x ] d[x] d[x] x x x 的深度,假设 d [ x ] > = d [ y ] d[x]>=d[y] d[x]>=d[y],用二进制思想把 x x x 调整到与 y y y 一个深度,每次走 2 l o g ( n ) − 0 2^{log(n)-0} 2log(n)0 步,保证 x x x 的深度 < = <= <= y y y 的深度,把当前节点标号赋值给 x x x,如果操作完后 x = y x=y x=y,则 y y y 就是LCA,如果不是,则开始同时把 x , y x,y x,y 用相同方式上调,保持二者不相汇,这样操作完后必定只有一步即可到二者的LCA,所以LCA即是 f [ x , 0 ] f[x,0] f[x,0]

LCA的Tarjan算法

本质上是用并查集对向上标记法进行优化。这是个离线算法,需要把 m m m 个询问一次性读入,统一计算,最后同一输出,时间复杂度为 O ( n + m ) O(n+m) O(n+m)
把节点分为三类:访问过且已经回溯的标记为2,访问过但未回溯的标记为1,未访问过的标记为0。
对于一个正在访问的节点 x x x,它到根节点的路径全标记为了1, l c a ( x , y ) lca(x,y) lca(x,y) 就是从 y y y 向上走到的第一个标记为1的节点。利用并查集进行优化,当一个节点被标记为2时。把它所在的集合合并到他父节点所在的集合当中(此时它的父节点显然被标记为了1且是一个独立的集合),这相当于每个完成回溯的节点都有一个指针指向它的父节点,这样只需查询 y y y 即可,如果 y y y 已经被标记为了2,那么说明查询的询问回答应是 y y y 在并查集中的代表元素。
再顺便提一下,还有一些求LCA的算法(树剖),但由于不经常用就不提了。

3、树上差分

原本学过的前缀和与差分也可以用在数上来做一些简化,但其中的区间操作要改为路径操作,前缀和改为子树和即可。

4.LCA综合应用

推荐题目:异象石,次小生成树,疫情控制。

四、负环与差分约束

1、负环

给定一张有向图,每条边都有一个权值,如果它是负数,那么称它为负权边,若图上存在一个环,环上各边的权值之和是负数,则称这个环为负环。
注意,DJ和Heap-DJ是不能判负环的。
而Bellma-Ford和SPFA可以判负环,因为如果存在负环,那么这两种算法无论经过多少次迭代,总存在有向边( x , y , z x,y,z x,y,z)使得 d [ y ] > d [ x ] + z d[y]>d[x]+z d[y]>d[x]+z,这样这两种算法就永远无法结束。再根据抽屉原理,若存在 d [ x ] d[x] d[x],从 1 1 1 开始的最短路包含 > = >= >= n n n 条边,那么这条路径必然经过了某个节点 p p p p p p 绕负环一次最终更新了自己,每绕一圈,最短路径只会越来越小,不可能收到最小。所以有以下判定法则:

Bellman-Ford判负环

经过 n n n 轮迭代,算法依旧没有结束,那么图中存在负环,若在 n − 1 n-1 n1 轮之内,算法结束,那么图中无负环。

SPFA判负环

c n t [ x ] cnt[x] cnt[x] 表示从 1 1 1 x x x 的最短路径包含的边数, c n t [ 1 ] = 0 cnt[1]=0 cnt[1]=0。在更新 d [ y ] d[y] d[y] 时,同时更新 c n t [ y ] cnt[y] cnt[y],若 c n t [ y ] > = n cnt[y]>=n cnt[y]>=n,则图中存在负环,若算法正常结束,则图中无负环。
两种判负环的方法时间复杂度最坏的时候都为 O ( n m ) O(nm) O(nm),当然SPFA会比Bellman-Ford稍快些。
SPFA另一种趴判负环的方法就是记录每个点入队次数,但时间复杂度不如上式优,不过有时可以两种方法混着用再加点卡时技巧来过题目。另外还有其他的优化手段,比如把SPFA的队列换成栈,把基于bfs改为基于dfs,但有时候时间复杂度反而会遍大,所以要慎用。

2、差分约束系统

这是一种特殊的 n n n 元一次不等式,它包含 n n n 个变量 x 1 , x 2 , . . . , x n x_1,x_2,...,x_n x1,x2,...,xn,以及 m m m 个约束条件,每个约束系统都是由两个变量作差构成的,形如 x i − x j < = c k x_i-x_j<=c_k xixj<=ck,其中 c k c_k ck 为常数,且 1 < = i , j < = n , 1 < = k < = m 1<=i,j<=n,1<=k<=m 1<=i,j<=n,1<=k<=m,而要解决的问题就是,求一组解 x 1 = a 1 , x 2 = a 2 , . . . , x n = a n x_1=a_1,x_2=a_2,...,x_n=a_n x1=a1,x2=a2,...,xn=an,满足所有约束条件,每个约束系统条件 x i − x j < = c k x_i-x_j<=c_k xixj<=ck 可以转化为 x i < = x j + c k x_i<=x_j+c_k xi<=xj+ck,这个与三角不等式十分相似,所以我们可以把每个变量 x i x_i xi 看做一个节点 i i i,每个约束条件 x i < = x j + c k x_i<=x_j+c_k xi<=xj+ck 可以化成从节点 j j j 到节点 i i i 连一条长度为 c k c_k ck 的有向边。
如果 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an 是一组解,那么 a 1 + p , a 2 + p , . . . , a n + p a_1+p,a_2+p,...,a_n+p a1+p,a2+p,...,an+p 也是一组解。那我们就可以先求一组负数解然后再扩大。
d [ 0 ] = 0 d[0]=0 d[0]=0,以 0 0 0 为起点求求单源最短路,如果有负环,则判无解否则 x i = d [ i ] x_i=d[i] xi=d[i] 就是差分约束系统的一组解。有些时候差分约束系统的约束会反过来,那么可以先转化再做,也可以求正环。

五、Tarjan算法与连通性

1、割点与桥

给定无向连通图 G = ( V , E ) G=(V,E) G=(V,E)
若对于 x ∈ V x\in V xV,从图中删去节点 x x x 以及所有与 x x x 有关的边之后, G G G 分裂成两个或以上不相连的子图,则称 x x x G G G 的割点。若对于 e ∈ E e\in E eE,从图中删去边 e e e 之后, G G G 分裂成两个不相连的子图,则称 e e e 是图 G G G 的桥或割边。一般无向图(可以不连通),它的割点割边就是它连通块的割点割边。
Tarjan算法(简称TJ)是基于无向图的深度优先遍历的,接下来介绍关于此的一些概念:

时间戳

在图的深度优先遍历过程中,按照每个节点第一次被访问的时间顺序,一次给予 n n n 个节点 1 − n 1-n 1n 的标记,记为 d f n [ x ] dfn[x] dfn[x]

搜索树

在无向连通图中任选一个节点出发进行深度优先遍历,每个点只访问一次。把所有发生递归的边 ( x , y ) (x,y) (x,y) 构成一棵树,把这棵树成为无向连通图的搜索树,不连通的图的各个连通块的搜索树构成了搜索森林。

追溯值

TJ算法还引入了一个追溯值,记为 l o w [ x ] low[x] low[x],定义为以 x x x 为根的子树中的节点和通过一条不在搜索树上的边能到达的点及其子树中的节点的最小时间戳。
为了计算 l o w [ x ] low[x] low[x],应该先令 l o w [ x ] = d f n [ x ] low[x]=dfn[x] low[x]=dfn[x],然后考虑从 x x x 出发的每一条边 ( x , y ) (x,y) (x,y),若在搜索树上 x x x y y y 的父节点, l o w [ x ] = m i n ( l o w [ x ] , l o w [ y ] ) low[x]=min(low[x],low[y]) low[x]=min(low[x],low[y]),若无向边 ( x , y ) (x,y) (x,y) 不是搜索树上的边, l o w [ x ] = m i n ( l o w [ x ] , d f n [ y ] ) low[x]=min(low[x],dfn[y]) low[x]=min(low[x],dfn[y])

割边判定法则

无向边是 ( x , y ) (x,y) (x,y) 是割边,当且仅当搜索树上存在 x x x 的一个子节点 y y y,满足 d f n [ x ] < l o w [ y ] dfn[x]dfn[x]<low[y]
根据定义,如果 d f n [ x ] < l o w [ y ] dfn[x]dfn[x]<low[y] 说明从 y y y 的子树出发,在不经过 ( x , y ) (x,y) (x,y) 的前提下,不管走那条边都无法达到 x x x 或者更早访问的节点,于是可以推出 ( x , y ) (x,y) (x,y) 为一条割边。
显然,割边一定是搜索树中的边,并且一个简单环中的边一定不是割边。
但是这里要注意一点,如果单处理每个节点的父节点,会无法处理重边的情况,这些边必定都不是割边,因为这些边中,最多只有一条边是搜索树上的边,其他几条都不算,所以 d f n [ f a ] dfn[fa] dfn[fa] 可以用来更新 l o w [ x ] low[x] low[x]。对于这种情况,一种比较好的解决方法就是改为记录递归进入每个节点的边的编号,编号可以认为是在领接表中储存的下标位置,把无向图的每一条边看做双向边,成对储存在下标2和3,4和5,6和7…若沿着编号为 i i i 的边递归进入了节点 x x x,则忽略从 x x x 出发的编号为 i i i x o r xor xor 1 1 1 的边,通过其他边进行计算即可。

割点判定法则

x x x 不是搜索树的根节点,则当且仅当搜索树上存在 x x x 的一个子节点 y y y 满足 d f n [ x ] < = l o w [ y ] dfn[x]<=low[y] dfn[x]<=low[y],那么 x x x 就是割点。如果它是根节点,那么需要至少存在两个 y y y 满足上述条件。
证明方法和割边的类似。

2、欧拉路问题

俗称一笔画问题。。。
s s s 出发到 t t t 的路径如果可以不重不漏的包含每条边一次(可重复经过某些节点),则称作欧拉路,如果 s s s t t t 重合,那么称作欧拉回路。

欧拉路的判定

给定一张无向图为欧拉图,当且仅当它是连通图而且每个点的入度都是偶数。

欧拉路的存在性判定

一张无向图上存在欧拉路,当且仅当有两个点度数为奇数,其他均为偶数,查找时可以借助dfs和栈。
说明,只要到达一个节点,因为度数是偶数,那么它必定还有对应的边可以出去。
时间复杂度为 O ( n m ) O(nm) O(nm),所以在领接表储存时可以通过及时改变指针来优化。另外,由于递归层数为 O ( m ) O(m) O(m) 级别的,容易造成栈溢出,所以要用另一个栈模拟递归过程。

3、强连通分量

给定一张图,若对于图中任意两个节点 x , y x,y x,y 存在从 x x x y y y 的路径也存在从 y y y x x x 的路径,则称之为强连通图。
有向图的极大强联通子图被称为强联通分量,简记为SCC。TJ算法基于有向图的dfs,能在线性时间内求出一张有向图的各个强连通分量。
显然一个环一定是强连通分量。所以TJ算法的基本思路就是对于每个点,尽量找到与它一起能构成环的所有节点。前向边 ( x , y ) (x,y) (x,y) 没什么用,因为他本来就在搜索树中,后向边 ( x , y ) (x,y) (x,y) 很有用,因为可以和搜索树上的从 y y y x x x 的边一起构成环。横叉边 ( x , y ) (x,y) (x,y) 要视情况而定,因为如果从 y y y 出发能找到一条路径回到 x x x 的祖先节点,那么 ( x , y ) (x,y) (x,y) 就是有用的。
为了找到通过后向边和横叉边构成的环,TJ算法在dfs的同时维护了一个栈,当访问到节点 x x x,栈中要保存两类节点:一类是搜索树上 x x x 的祖先节点,若 y y y 包含在里面并且存在后向边 ( x , y ) (x,y) (x,y) y y y x x x 的路径一起形成环;另一类是已经访问过的节点 z z z 且存在一条路径能到达 x x x 的祖先节点 y y y,此时若存在横叉边 ( x , z ) (x,z) (x,z) 则能发现一个环。

追溯值

由上引入了这个概念, x x x 的追溯值 l o w [ x ] low[x] low[x] 定义为满足该点在栈中或者存在一条从以 x x x 为根的子树出发的以该点为终点的最小的时间戳。计算追溯值时,当 x x x 第一次被访问到时,把 x x x 入栈,初始化 l o w [ x ] = d f n [ x ] low[x]=dfn[x] low[x]=dfn[x],扫描从 x x x 出去的每条边 ( x , y ) (x,y) (x,y),如果 y y y 没有被访问过,说明它是搜索树上的边,递归访问 y y y,回溯的时候 l o w [ x ] = m i n ( l o w [ x ] , l o w [ y ] ) low[x]=min(low[x],low[y]) low[x]=min(low[x],low[y]),如果 y y y 被访问过了且在栈里,则令 l o w [ x ] = m i n ( l o w [ x ] , d f n [ y ] ) low[x]=min(low[x],dfn[y]) low[x]=min(low[x],dfn[y]),从 x x x 回溯前,判断是否有 l o w [ x ] = d f n [ x ] low[x]=dfn[x] low[x]=dfn[x],如果成立,则不断从栈中弹出节点,知道 x x x 出栈。

强连通分量判定法则

解释一下,在追溯值计算的过程中,若从 x x x 回溯前,有 l o w [ x ] = d f n [ x ] low[x]=dfn[x] low[x]=dfn[x] 成立,则栈中从 x x x 到栈顶的所有节点构成一个强连通分量,因为如果 l o w [ x ] = d f n [ x ] low[x]=dfn[x] low[x]=dfn[x],那么说明以 x x x 为根的子树中的节点不能再与栈中的其他节点一起构成环,而横叉边的终点时间戳必定小于起点时间戳,所以以 x x x 为根的子树中的节点也不可能直接到达尚未访问的节点,所以栈中从 x x x 到栈顶的所有节点不能与其他节点一起构成环。而又因为及时判定出栈,那么这些点必定是独立构成一个强联通分量的。
啊,终究是结束了。果然,我写到一半就忘记写了,一咕这么久。。。刚好csp初赛要开始了,图论身为比较重要的考点那就再来复习一下吧。

然后准备经受初赛的洗礼吧!!!

你可能感兴趣的:(初赛问题,总结)