Tarjan 算法及其应用
NO.1 求强连通分量
学习链接: https://www.cnblogs.com/shadowland/p/5872257.html
学习心得: dfn[cur] 记录访问 cur 结点的时间戳,low[cur] 记录 cur 结点及其子树中时间戳最小是多少,严格意义上来讲low[cur],记录的是在不回头遍历父节点的前提下第一次能访问到的最早的已遍历结点的时间戳。显然当访问 cur 结点的子节点 to 时,若 dfn[to] 不为0,则 to 结点到 cur 或 cur 的祖先结点中必然存在环,亦即到 to 结点为止的递归栈中的所有结点可以构成一个强连通分量。因为要先处理子节点后才能得出上层结点的 low,因此必然用后序遍历。具体的细节证明可参考链接。
画张图解释下上述low严格意义上的含义。假设现在建无向边(加边时建正向反向各建一次边),建边:6点、6条双向边
1 2
1 3
3 4
3 5
4 6
5 6
结果:遍历顺序1->3->5->6->4->(到3,回溯改4、6、5)->2,这里dfs序中3可看作4的虚子结点,即3在4、5、6的子树中,1、2不在。
代码实现:
#include <bits/stdc++.h>
#define ll long long
#define INF 0x3f3f3f3f
#define mem( f, x ) memset( f, x, sizeof( f ) )
#define pii pair<int, int>
#define fi first
#define se second
#define mk(x, y) make_pair( x, y )
#define pk push_back
using namespace std;
const int M = 1e5 + 5;
const int N = 1e5 + 5;
int m, n, cnt, ck_num, col_num;
int head[N], dfn[N], low[N], color[N];
bool vis[N];
int sk[N], top;
struct eg{
int to, pre;
eg( ){
to = pre = 0; }
eg( int tt, int pp ){
to = tt, pre = pp;
}
}e[2*M];
void add( int x, int y ){
e[++cnt] = eg( y, head[x] );
head[x] = cnt;
}
void Tarjan( int cur ){
dfn[cur] = low[cur] = ++ck_num;
vis[cur] = 1;
sk[++top] = cur;
for( int i = head[cur]; i; i = e[i].pre ){
int to = e[i].to;
if( !dfn[to] ){
Tarjan( to ); //后序遍历
low[cur] = min( low[cur], low[to] ); //根据子树更新当前结点中的low[cur]
}
else if( vis[to] )
low[cur] = min( low[cur], dfn[to] ); //邻接结点在递归栈中时,根据邻接结点更新当前结点的low
//(已遍历至终点,终点low可直接根据其已成为祖先的邻接结点的时间戳直接更新low)
}
if( dfn[cur] == low[cur] ){
color[cur] = ++col_num;
vis[cur] = 0; //cur结点出栈是一定要记得
while( sk[top] != cur ){
color[sk[top]] = col_num;
vis[sk[top--]] = 0;
}
top--;
}
}
void init( ){
for( int i = 0; i <= n; i++ )
head[i] = dnf[i] = low[i] = vis[i] = color[i] = 0;
cnt = top = ck_num = col_num = 0;
}
int main( ){
while( scanf( "%d %d", &n, &m ) != EOF ){
init( );
int x, y;
for( int i = 0; i < m; i++ ){
scanf( "%d %d", &x, &y );
add( x, y