【模板】Tarjan


\(\color{black}{Tarjan}\)是在\(dfs\)的过程中维护了一个栈,以及\(low\)(回溯值)\(dfn\)(时间戳)两个数组。

\(low\)\(dfn\)的计算

\(dfn\)就在\(dfs\)的过程中打就可以了,而\(low\)的计算方式稍稍复杂一些:

1.如果\(x-y\)为树枝边(\(y\)未被访问),则\(low_x = \min(low_x,low_y)\)

2.如果\(x-y\)为返祖边(\(y\)在栈中),则\(low_x = \min(low_x,dfn_y)\)

3.除此之外,啥都不干

强连通分量

在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通。

如果有向图G的每两个顶点都强连通,称G是一个强连通图。

非强连通图有向图的极大强连通子图,称为强连通分量。

则通过手模易得:\(\text{当一个点}x\text{经过}dfs\text{后},dfn_x =low_x,\text{则栈中}x\text{以上的点构成一个强连通分量}\)

bool instk[z];
int ti, dfn[z], low[z], belong[z];
stack stk;
void tarjan(int u) {
	dfn[u] = low[u] = ++ti;
	stk.push(u);
	instk[u] = true;
	for(int i = head[u];i;i = edge[i].next) {
		if(!dfn[edge[i].t]) {
			tarjan(edge[i].t);
			low[u] = min(low[u],low[edge[i].t]);
		} else if(instk[edge[i].t]) {
			low[u] = min(low[u],dfn[edge[i].t]);
		}
	}
	if(low[u] == dfn[u]) {
		int tmp;
		++belong[0];
		do {
			tmp = stk.top();
			stk.pop();
			instk[tmp] = false;
			belong[tmp] = belong[0];
		} while(tmp != u);
	}
}

割点

若删掉某点后,原连通图分裂为多个子图,则称该点为割点。

则易得:\(\text{若一点}x\text{的}low_x \ge dfn_x,\text{则}x\text{为割点}\)

bool cutPoint[z];
int ti, root, dfn[z], low[z];
void tarjan(int u) {
	dfn[u] = low[u] = ++ti;
	int son = 0;
	for(int i = head[u];i;i = edge[i].next) {
		if(!dfn[edge[i].t]) {
			++son;
			tarjan(edge[i].t);
			low[u] = min(low[u],low[edge[i].t]);
			if(dfn[u] <= low[edge[i].t]) 
				if(u != root||son > 1) cutPoint[u] = true;
				//注意如果u为起点,至少有两棵子树才算割点;
		} else if(instk[edge[i].t]) {
			low[u] = min(low[u],dfn[edge[i].t]);
		}
	}
}

割边(桥)

删掉某边之后,图会分裂为两个或两个以上的子图,则该边为桥。

则易得:\(\text{若某边}x-y\text{有}low_x > dfn_y,\text{则该边为割边}\)

进一步,则有

1.\(low_x = dfn_x\)

2.\(dfn_x < low_y\)

3.\(\text{前两种综合,}\)

bool cutEdge[z];
int ti, dfn[z], low[z], cut, pEdge[z];
void tarjan(int u) {
	dfn[u] = low[u] = ++ti;
	for(int i = head[u];i;i = edge[i].next) {
		if(i == (pEdge[u]^1)) continue;
		if(!dfn[edge[i].t]) {
			pEdge[edge[i].t] = i;
			tarjan(edge[i].t);
			low[u] = min(low[u],low[edge[i].t]);
			if(dfn[u] > low[edge[i].t]) {
				cut++;
				cutEdge[i] = cutEdge[i^1] = true;
			}
		} else {
			low[u] = min(low[u],dfn[edge[i].t]);
		}
	}
}

点、边双连通分量

若一个无向图中的去掉任意一个节点(一条边)都不会改变此图的连通性,即不存在割点(桥),则称作点(边)双连通图。

一个无向图中的每一个极大点(边)双连通子图称作此无向图的点(边)双连通分量。求双连通分量可用\(Tarjan\)算法。

1.点双联通分量

对于点双连通分支,实际上在求割点的过程中就能顺便把每个点双连通分支求出。建立一个栈,存储当前双连通分支,访问每个点时把这个点入栈。

如果某时满足\(dfn_u\le low_v\),说明u是一个割点,那么把点从栈顶一个个取出,直到遇到了点\(u\)\(u\)不取出),取出的这些点和\(u\)组成一个点双连通分支。

割点可以属于多个点双连通分支,其余点和每条边只属于且属于一个点双连通分支。

bool cutPoint[z];
int ti, dfn[z], low[z], cut, root;
stack stk;
vector pbcc[z], belong[z];
void tarjan(int u,int p) {
	dfn[u] = low[u] = ++ti;
	stk.push(u);
	bool first = true;
	int sub = 0;
	for(int i = head[u];i;i = edge[i].next) {
		if(first&&edge[i].t == p) {
			first = false;
			continue;
		}
		if(!dfn[edge[i].t]) {
			++sub;
			tarjan(edge[i].t,u);
			low[u] = min(low[u],low[edge[i].t]);
			if(dfn[u] <= low[edge[i].t]) {
				cutPoint[u] = true;
				cut++;
				pbcc[cut].push_back(u);
				int tmp;
				do {
					tmp = stk.top();
					stk.pop();
					pbcc[cut].push_back(tmp);
					belong[tmp].push_back(cut);
				} while(tmp != edge[i].t);
			}
		} else {
			low[u] = min(low[u],dfn[edge[i].t]);
		}
	}
	if(u == root&&sub == 1) 
		cutPoint[u] = false;
}

2.边双连通分量

对于边双连通分支,求法更为简单。只需在求出所有的桥以后,把桥边删除,原图变成了多个连通块,则每个连通块就是一个边双连通分支。

桥不属于任何一个边双连通分支,其余的边和每个顶点都属于且只属于一个边双连通分支。

stack stk;//栈;
vector ebc[z];//存每一个边双联通分量;
bool cut[z];//边是否为桥;
int dfn[z], low[z], ti;
int belong[z];//判断某点在哪一个分量里;
void tarjan(int &u,int &pid) {
    dfn[u] = low[u] = ++ti;
    stk.push(u);
    for(int i = head[u];i;i = edge[i].next) {
        if(i == (pid^1)) continue;//WARNING!加括号!
        if(!dfn[edge[i].t]) {
            tarjan(edge[i].t,i);//顺便这里判的一直是边的id;
            low[u] = min(low[u],low[edge[i].t]);
        } else {
            low[u] = min(low[u],dfn[edge[i].t]);
        }
    }
    if(dfn[u] == low[u]) {//发现有桥,开始找分量;
        cut[pid] = true;
        ++belong[0];
        int tmp;
        do {
            tmp = stk.top();
            stk.pop();
            belong[tmp] = belong[0];
            ebc[belong[0]].push_back(tmp);
        } while(tmp != u);
    }
    return;
}

3.构造边双连通分量

若一个有桥的连通图的叶子结点数量为\(n\),则至少要加\(\left\lfloor\dfrac{n+1}{2}\right\rfloor\)条边就能使该图变为边双连通图。

具体方法为,首先在两个最近公共祖先最远的两个叶节点之间连接一条边,把这两个点到祖先的路径上所有点收缩到一起,再继续祖先最远的两个叶节点,以此类推。

LCA(施工中)

我们先读入所有的询问并对这些询问构建一个邻接表。

在遍历到u时,先tarjan遍历完u的子树,则u和u的子树中的节点的最近公共祖先就是u,并且uu的兄弟节点及其子树的最近公共祖先就是u的父亲。

用一个color数组,正在访问的节点标记为1,未访问的标记为0,已经访问到的即在u的子树中的u的已访问的兄弟节点及其子树中的标记为2。

再维护一个并查集,访问完节点u的一个子树时,就把这个子树的根节点的fa改为u。访问完u的所有子树后,考虑所有与u相关的询问lca(u,v),那么lca(u,v)就是v所在并查集的根。

这是一个离线算法,时间复杂度为O(Nα(N)),约为O(N)。

int p[z], color[z];
void tarjan(int u) {
	p[u] = u;
	color[u] = 1;
	int y;
	for(int i = head[u];i;i = edge[i].next) {
		if(color[y = ver[i]] == 0) {
			tarjan(y);
			p[y] = x;
		}
	}
	for(int i = headquery[u];i;i = query[i].next) {
		if(color[y = query[i]] == 2) ans[i] = get(y);
	}
	color[u] = 2;
}