tarjan的部分讲解及有关证明解答

LCA(Tarjan)
分类,使每个结点都落到某个类中,到时候只要执行集合查询,就可以知道结点的LCA了。
对于一个结点u.类别有:
以u为根的子树、除类一以外的以f(u)为根的子树、除前两类以外的以f(f(u))为根的子树、除前三类以外的以f(f(f(u)))为根的子树……
类一的LCA为u,类二为f(u),类三为f(f(u)),类四为f(f(f(u)))。这样的分类看起来好像并不困难。
但关键是查询是二维的,并没有一个确定的u。接下来就是这个算法的巧妙之处了。
利用递归的LCA过程。
假设lca(u)执行完毕,则以u为根的子树已经全部并为了一个集合。而一个lca的内部实际上做了的事就是对其子结点,依此调用lca.
设v1,v2,v3…vn为u的后继结点且访问顺序为v1,v2,v3…vn
当v1(第一个子结点)被lca,正在处理v2的时候,以v1为根的子树+u同在一个集合里,f(u)+编号比u小的u的兄弟的子树 同在一个集合里,f(f(u)) + 编号比f(u)小的 f(u)的兄弟 的子树 同在一个集合里……
而这些集合,对于v2的LCA都是不同的。因此只要查询x在哪一个集合里,就能知道LCA(v2,x)
还有一种可能,x不在任何集合里。当他是v2的儿子,v3,v4等子树或编号比u大的u的兄弟的子树(等等)时,就会发生这种情况。即还没有被处理。还没有处理过的怎么办?把一个查询(x1,x2)往查询列表里添加两次,一次添加到x1的回答列表里,一次添加到x2的回答列表里,如果在做x1的时候发现 x2已经被处理了,那就接受这个询问,未被处理就忽略。(两次中必定只有一次询问被接受).
复杂度为O(n+Qusetion times)Qusetion times为询问次数

Ps:emmmm,以上内容来自度娘,不计入总字数
当然是不可能的

好吧不讲废话了

before

我们先看一下tarjan的系统定位,比如:

那么问题来了,强联通分量是个什么诡异的东西?
那就像

强连通(strongly connected): 在一个有向图G里,设两个点 a b 发现,由a有一条路可以走到b,由b又有一条路可以走到a,我们就叫这两个顶点(a,b)强连通。
强连通图: 如果 在一个有向图G中,每两个点都强连通,我们就叫这个图,强连通图。
强连通分量(strongly connected components):在一个有向图G中,有一个子图,这个子图每2个点都满足强连通,我们就叫这个子图叫做 强连通分量 [分量::把一个向量分解成几个方向的向量的和,那些方向上的向量就叫做该向量(未分解前的向量)的分量

从这个定义中,我们可以知道,当一个图/分量是强连通图/分量,那么它的每一个点都在一个点权>=2的环里。(除非它是一个单点图/分量)
真拓麻有趣

好的,举个粒子:
4 5//四个节点五条边
1->2
2->3
3->1
1->4
4->2

对不起,这里有必要的让大家想想一下,在本图中,1点可以经过1->2->3->1的路径回到1点,同理,2,3,4也都可以,所以{1,2,3,4}是一个强联通分量。

有必要提及一点,即单独一个点也是强联通分量。

SO?现在我们需要迅速地分析一下tarjan算法的真正打开方式↓

then


考虑一下图本图?
怎样才能够更好的搜索到一整个有向图?很明显,DFS是较为完美的一种做法,如果你尝试使用BFS,那么你的程序可能面临T的危机(初步预算),如果是DFS,他可以将每个强连通分量当成一株子树,从而达到BFS没有的效果(我们的目标是追求高配)。
为了更好地使搜索进行下去,我们需要赋予每个点两个参数:
DNF[]表示该节点的访问时间
LOW[]表示每个点在这颗树中的,最小的子树的根,emmmmm,或者可以认为是祂父亲的LOW的那种想法,如果它自己的LOW[]最小,那这个点就应该重新分配,变成这个强连通分量子树的根节点。

为了更加容易地储存强连通分量,我们特意选择了栈。每次一个新节点出现,就进站,如果这个点有 出度 就继续往下找。直到找到底,每次返回上来都看一看子节点与这个节点的LOW值,谁小就取谁,保证最小的子树根。最后找到强连通分量的节点后,就将这个栈里,比此节点后进来的节点全部出栈,它们就组成一个全新的强连通分量,比如说:
tarjan(u){
DFN[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值
Stack.push(u) // 将节点u压入栈中
   for each (u, v) in E // 枚举每一条边
    if (v is not visted) // 如果节点v未被访问过
        tarjan(v) // 继续向下找
        Low[u] = min(Low[u], Low[v])
    else if (v in S) // 如果节点u还在栈内
        Low[u] = min(Low[u], DFN[v])
  if (DFN[u] == Low[u]) // 如果节点u是强连通分量的根
  repeat v = S.pop // 将v退栈,为该强连通分量中一个顶点
  print v
  until (u== v)
}

在这里插入图片描述
对不起,还是图本图
此时,1入栈,栈内さ 1
DFN[1] = LOW[1] = ++index = = = = = 1;
然后,2入栈,栈内さ 1 2
DFN[2] = LOW[2] = ++index = = = = = 2;
继续,3入栈,栈内さ 1 2 3
DFN[3] = LOW[3] = ++index = = = = = 3;
继续,6入栈,栈内さ 1 2 3 6
DFN[6] = LOW[6] = ++index = = = = = 4;

然后,emmm,6没有出边?
那就说明6是强连通分量の根,6 & 6以后的节点出栈!
栈内さ 1 2 3
诶,3也是根节点,3 & 3以后的节点出栈!
栈内さ 1 2
回到 2节点之后,继续,5入栈,栈内さ 1 2 5
DNF[5] = LOW[5] = ++index = = = = = 5;
从 5开始走会发现 6,观察到 6都被访问过,不管它;
继续,发现 5可以走到 1,也就是祂的爷爷,DFN[1]被访问过并且还在栈中,说明1还在这个强连通分量中,所以,Low[5] = min(Low[5], DFN[1])
好的,因为DFN[5]>DFN[1],所以 1是 5的祖先,LOW[5] = 1;
回溯到 2,LOW[2] = 1;
回溯到 1,1还有未访问出边,继续,4入栈,栈内さ 1 2 5 4
DFN[4] = LOW[4] = ++index = = = = = 6;
继续,走到 5,发现 5已被访问,所以Low[4] = min(Low[4], DFN[5])
所以 4是 5的一个子节点,LOW[4] = 5
回溯到 1,发现LOW[1] = DFN[1],那就说明 1是强连通分量の根,1 & 1以后的节点出栈!
栈为空。
还有呢?
继续
接着
新节奏

你以为这样就玩完了吗?
不,万一你走了一遍tarjan但并没有访问完一整个图怎么办?
所以现在应寻找未被访问过的节点,继续tarjan之旅。
但很明显,这张图本图中并不包含这种情况(除非你执意要从 6开始执行)。
因此友提:最好将tarjan放在循环中运行。

do

来一道纯tarjan:

输入:有向图的常规操作
输出:每行一个强连通分量

input:

6 8
1 2
2 3
1 4
2 5
4 5
3 6
5 6
5 1

在这里插入图片描述
就是这个图本图的数据

output:

6
3
4 5 2 1
//(Ps:顺序不限)

#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
struct node {
	int v,next;
}edge[1010];
int DFN[1010],LOW[1010],
stack[1010],heads[1010],visit[1010],cnt,index,init,
n,m,x,y;

void add(int x,int y) {
	edge[++cnt].next = heads[x];
	edge[cnt].v = y;
	heads[x] = cnt;
	return;
}

void tarjan(int x) {
	DFN[x]=LOW[x]=++index;
	stack[++init]=x;
	visit[x]=1;
	for (int i=heads[x];i!=-1;i=edge[i].next) {
		if (!DFN[edge[i].v]) {
			tarjan(edge[i].v);
			LOW[x]=min(LOW[x],LOW[edge[i].v]);
		}else if (visit[edge[i].v]) {
			LOW[x]=min(LOW[x],LOW[edge[i].v]);
		}
	}if (LOW[x] == DFN[x]) {
		while (x != stack[init+1]) {
			printf("%d ",stack[init])
			visit[stack[init--]]=0;
		}printf("/n");
	}return;
}

int main() {
	memset(heads,-1,sizeof heads);
	scanf("%d%d",n,m);
	for (int i=1;i<=m;i++) {
		scanf("%d%d",x,y);
		add(x,y);
	}for (int i=1;i<=n;i++) {
		if (!DFN[i]) tarjan(i)
	}
	return 0;
}

堪称完美

那么我们继续接下来的事

prove

首先,我们先引入关于强连通分量的定理:
定理1:一个完整的强连通分量一定包含在一棵深度优先搜索树中。
定理2:子图是强连通分量<=>子图中的每一条路径都归属于一个环状(除非只有一个点)。
证明:根据强连通分量的定义,任意两个点之间可以互达,所以等价于任意两个点之间的路径是一个环的一部分。

很好。
那么为什么每次出栈的部分都是一个强连通分量?
根据定理2,等价于证明,出栈的部分,节点路径都是某个环的一部分。

已知:目前栈内的将出栈部分,假如节点下标为i,则一定有dfn[i]>=low[i](当且仅当i=最后一个出栈的节点下标时取等号)。
运用反证法:假如有某条路径不属于某个环,如下图中的路径B->A,则这段路径的末端点A的dfn[A] = low[A],且该点是出栈的某条路径的末端点,而不是初始点,所以不可能是最后出栈的那个点,和已知矛盾。所以,出栈的任意一条路径都属于某个环,即出栈的子图是一个强连通分量。

那么为什么每次输出的一定是一个完整的强连通分量?

分析:如果不完整,那么剩余的部分应该为?
A.还未入栈的部分
B.不准备出栈的部分
C.已经出栈的部分

首先,A不可能。如果祂未入栈,则祂不是这个子树中的一部分,更不可能属于该强连通分量。

其次,B也不可能。因为根据我们上面对tarjan的分析,不准备出栈的节点的DFN比目前强连通分量的根节点的DFN小,明显是没有更新的结果,但如果祂属于这个强连通分量,这必然会被子孙访问到而被更新,所以不科学。

最后再看C。之前出栈的部分X如果和本次出栈的强连通分量Y可以组成更大的强连通分量,这就等价于,以之前出栈的强连通分量X为视角,亦是说X是不完整的,X中缺少的部分在还未出栈的节点和还没有访问的节点之中。这和之前的选项A,选项B推导矛盾,所以,C也不可能。

∴每次出栈的部分必定是一个完整的强连通分量。

all proved

点个在看吧↓

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值