众所周知,N 个点的树有 N-1 条边。若在树上任意添加一条边,则会形成一个环。除了环之外,其余部分由若干棵子树构成。
我们把这种 N 个点 N 条边的连通无向图,即在树上加一条边构成的恰好包含一个环的图,称为“基环树”。如果不保证连通,那么 N 个点 N 条边的无向图也可能是若干棵基环树组成的森林,简称为“基环树森林”。
在有向图中,我们也有类似的概念。N 个点、N 条边、每个节点有且只有一条入边的有向图就好像以“基环”为中心,有向外扩散的趋势,故称为“外向树”。N个点、N条边、每个节点有且只有一条出边的有向连通图就好像以“基环”为中心,有向内收敛的趋势,故称为“内向树”。外向树和内向树也经常统称为“基环树”。如果不保证连通,那么 N 个点、N 条边、每个节点有且只有一条出(入)边的有向图也可能是“内(外)向树森林”的形态。
基环树的结构比常规的树要复杂,因此常作为一些经典模型的扩展,以适当增加题目的难度。例如基环树的直径、基环树两点间的距离、基环树动态规划等。无论哪种模型,求解基环树相关问题的方法一般就是先找出图中唯一的环,把环作为基环树的广义“根节点”,把除了环之外的部分按照若干棵树处理,在考虑与环一起计算。
先来定义一下基环树的直径。基环树中最长的简单路径被称为基环树的最长链,其长度被称为基环树的直径。这里,简单路径指的是不自交(不重复经过任何点或边)的路径。
如何求出基环树的最长链呢?显然,基环树的最长链可能有两种情况:
我们先用一次深度优先搜索遍历找出基环树中的“环”,把“环”上的节点做上标记。
设环上的节点为 s1,s2,s3,...,st。
从每个 si 出发,在不经过环上其他节点的前提下,再次执行深度优先遍历,即可访问去掉“环”之后以 si 为根的子树。在这样的每棵子树中,按照求树的直径的方法进行 dp 并更新答案,即可处理第一种情况。同时,还可以计算出 D[si],表示从节点 si 出发走向以 si 为根的子树,能够到达的最远节点的距离。
最后,我们考虑第二种情况。这相当于找到环上两个不同的节点 si,sj,使得 D[si] + D[sj] +dist(si,sj) 最大。其中 dist(si,sj) 表示 si,sj 在环上的距离,有逆时针、顺时针两种走法,取较长的那一种。
给定一张有向图(无向图的每条边可以看作两条方向相反的有向边从而按照有向图处理),每条边都有一个权值(长度)。若一条边的权值是负数,则称它是负权边。若图中存在一个环,环上各边的权值之和是负数,则称这个环为“负环”。
如果图中存在负环,那么直观表现为:无论经过多少轮迭代,总存在有向边(x,y,z),使得 dist[y] > dist[x] + z,Bellman - Ford 与 SPFA 算法永远不能结束。
根据抽屉原理,若存在一个 dist[x],从起点 1 到节点 x 的最短路包含 ≥ n 条边,则这条路径必然重复经过了某个节点 p。换言之,这条最短路径上存在一个环,环上各点都能更新下一个点的 dist 值。p 绕这个环一圈,最终能更新它自己。因此,这个环的总长度是负数。每绕一圈,最短路长度只会越来越少,不可能收收敛到每条边都满足三角形不等式的状态。
若经过 n 轮迭代,算法仍未结束(仍有能产生更新的边),则图中存在负环。
若 n-1 轮迭代之内,算法结束(所有边满足三角形不等式),则图中无负环。
设 cnt[x] 表示从 1 到 x 的最短路径包含的边数,cnt[1]=0.当执行更新 dist[y] = dist[x] + z 时,同样更新 cnt[y] = cnt[x] + 1。此时若发现 cnt[y] ≥ n,则图中有负环。若算法正常结束,则图中无负环
差分约束系统是一种特殊的 N 元一次不等式组。它包含 N 个变量 X1~XN 以及 M 个约束条件,每个约束条件都是由两个变量作差构成的,形如 Xi - Xj ≤ ck,其中 ck 是常数(可以是非负数,也可以是负数),1≤i,j≤N,1≤k≤M。我们要解决的问题就是:求一组解 X1=a1,X2=a2...Xn=an,使所有约束条件都得到满足。
差分约束系统的每个约束条件 Xi - Xj ≤ ck 可以变形为 Xi ≤ Xj + ck。这与单源最短路经问题中的三角形不等式 dist[y] ≤ dist[x] + z 非常相似。因此,可以把每个变量 Xi 看作有向图中的一个节点 i,对于每个约束条件 Xi - Xj ≤ ck,从节点 j 向节点 i 连一条长度为 ck 的有向边。
注意到如果{a1,a2,a3,...,an}是一组解,那么对任意的常数▲,{a1+▲,...,an+▲}显然也是一组解(作差后▲恰好被消掉)。所以,不妨先求一组负数解,即假设任意 i,Xi ≤ 0,然后再增加一个 0 号节点,令 X0=0。这样一来,就多了 N 个形如 Xi - X0 ≤ 0 的约束条件,应该从节点 0 向每个节点 i 连一条长度为 0 的有向边。
设 dist[0] = 0,以 0 为起点求单源最短路。若图中存在负环,则给定的差分约束系统无解。否则,Xi = dist[i] 就是差分约束系统的一组解。
给定无向连通图 G=(V,E):
若对于x∈V,从图中删去节点 x 以及所有与 x 关联的边之后,G分裂成两个或两个以上不想连的子图,则称 x 为 G 的割点。
若对于 e∈E,从图中删去边 e 之后,G分裂成两个不相连的子图,则称 e 为 G 的桥或割边。
一般无向图(不一定连通)的“割点”和“桥”就是它的各个连通块的“割点”和“桥”。
根据著名计算机科学家 Robert Tarjan 的名字命名的 Tarjan 算法能够在线性时间内求出无向图的割点与桥,进一步可求出无向图的双连通分量。在有向图方面,Tarjan 算法能够求出有向图的强连通分量,必经点与必经边。Robert Tarjan 在数据结构方面也做出了很多有成效的工作,包括证明并查集的时间复杂度,提出斐波那契堆,Splay Tree 和 Lint-Cut Tree等。
Tarjan 算法基于无向图的深度优先遍历,在此之前,我们应该了解了“时间戳”的概念。
在图的深度优先遍历过程中,按照每个节点第一次被访问的时间顺序,依次给予N个节点 1~N 的整数标记,该标记就被称为“时间戳”,记为 dfn[x]
在无向连通图中任选一个节点出发进行深度优先遍历,每个点只访问一次。所有发生递归的边(x,y)(换言之,从 x 到 y 是对 y 的第一次访问)构成一棵树,我们把它称为“无向连通图的搜索树”。当然,一般无向图(不一定连通)的各个连通块的搜索树构成无向图的“搜索森林 ”。
除了时间戳之外,Tarjan 算法还引入了一个“追溯值”low[x]。设 subtree(x) 表示搜索树中以 x 为根的子树。low[x] 定义为以下节点的时间戳的最小值:
1. subtree(x) 中的节点。
2. 通过1条不在搜索树上的边,能够到达 subtree(x) 的节点。
根据定义,为了计算 low[x],应该先令 low[x] = dfn[x],然后考虑从 x 出发的每条边(x,y):
若在搜索树上 x 是 y 的父节点,则令 low[x] = min(low[x],low[[y])
若无向边(x,y)不是搜索树上的边,则令 low[x] = min(low[x],dfn[y])
无向边(x,y)是桥,当且仅当搜索树上存在 x 的一个子节点 y,满足: dfn[x] < low[y]
根据定义,dfn[x] < low[y] 说明从 subtree(y) 出发,在不经过 (x,y) 的前提下,不管走哪条边,都无法到达 x 或比 x 更早访问的节点。若把(x,y)删除,则 subtree(y) 就好像形成了一个封闭的环境,与节点 x 没有边相连,图断开成了两部分,因此(x,y)是割边。
反之,若不存在这样的子节点 y,使得 dfn[x] 也就是说桥一定是搜索树中的边,并且一个简单环中的边一定都不是桥。 特别需要注意,因为遍历的是无向图,所以从每个点 x 出发总能访问到它的父节点 fa。根据 low 的计算方式,(x,fa) 属于搜索树上的边,且 fa 不是 x 的子节点,故不能用 fa 的时间戳来更新 low[x]。但是,如果仅记录每个节点的父节点,会无法处理重边的情况——当 x 与 fa 之间有多条边时,(x,fa) 一定不是桥。在这些重复的边中,只有一条算是“搜索树上的边”,其他的几条都不算。故有重边时,dfn[fa] 能用来更新 low[x]。 若 x 不是搜索树的根节点(深度优先遍历的起点),则 x 是割点当且仅当搜索树上存在 x 的一个子节点 y,满足:dfn[x] ≤ low[y] 特别地,若 x 是搜索树的根节点,则 x 是割点当且仅当搜索树上存在至少两个子节点 y1,y2 满足上述条件。 证明方法与割边的情形类似,这里就不再赘述。在“割边判定法则”画出的例子中,共有2个割点,分别是时间戳为 1 和 6 的两个点。 因为割点判定法则是小于等于号,所以在求割点时,不必考虑父节点和重边的问题,从 x 出发能访问到的所有点的时间戳都可以用来更新 low[x] 若一张无向连通图不存在割点,则称它为“点双连通图”。若一张无向连通图不存在桥,则称它为“边双连通图”。 无向图的极大点双连通子图被称为“点双连通分量”,简记为“v-DDC”。无向连通图的极大边双连通子图称为“边双连通分量”,简记为“e-DCC”。二者统称为“双连通分量”,简记为“DCC”。void tarjan(int x,int in_edge){
dfn[x] = low[x] = ++num;
for(int i=head[x];i;i=e[i].nxt){
int y=e[i].to;
if(!dfn[y]){
tarjan(y,i);
low[x]=min(low[x],low[y]);
if(low[y]>dfn[x]) bridge[i]=bridge[i^1]=true;
}
else if(i!=(in_edge^1)) low[x]=min(low[x],dfn[y]);
}
}
割点判定法则
void tarjan(int x){
dfn[x]=low[x]=++num;
int flag=0;
for(int i=head[x];i;i=e[i].nxt){
int y=e[i].to;
if(!dfn[y]){
tarjan(y);
low[x]=min(low[x],low[y]);
if(low[y]>=dfn[x]){
flag++;
if(x!=root||flag>1) cut[x]=1;
}
}
else low[x]=min(low[x],dfn[y]):
}
}
无向图的双连通分量