Tarjan 算法
有向图的强连通分量 SCC
定义:从其中的任意一个节点出发,都能经过其中的所有点,即其中任意两个节点相通。
遍历方式:DFS 序遍历。
有向边分类:树枝边,前向边,后向边,横叉边。
时间戳与追溯值:记 \(dfn[x]\) 为点 \(x\) 的时间戳(被访问的顺序),\(low[x]\) 为点 \(x\) 的追溯值,用它来存储不经过其父亲能到达的最小的时间戳。
强连通分量的判定:若从 \(x\) 回溯前,有 \(low[x]=dfn[x]\) 成立,则此时栈中从x到栈顶的所有节点构成一个强连通分量。
代码实现:
const int N=1e4+10;
vector<int> q[N];
stack<int> s;
int dfn[N],low[N],scc[N],siz[N],t,cnt,ans;
void tarjan(int x){
dfn[x]=low[x]=++t;
s.push(x);
for(int i=0;i<q[x].size();i++){
int y=q[x][i];
if(!dfn[y]){
tarjan(y);
low[x]=min(low[x],low[y]);
}
else if(!scc[y]) low[x]=min(low[x],dfn[y]);
}
if(dfn[x]==low[x]){
cnt++;
while(1){
int y=s.top();
s.pop();
scc[y]=cnt;
siz[cnt]++;
if(y==x) break;
}
}
}
int main(){
int n,m,x,y;
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>x>>y;
q[x].push_back(y);
}
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
以上代码中,\(cnt\) 计算的是 SCC 的个数(编号),\(scc[i]\) 则记录第 \(i\)个节点属于哪一个 SCC,\(siz[cnt]\) 记录的是每个 SCC 的大小。
因为图不一定连通,所以只要没有被记录时间戳的节点都要 \(Tarjan\) 一遍。
例题:P2863 [USACO06JAN] The Cow Prom S
缩点
我们可以把每个 SCC 缩成一个点。对原图中的每条有向边 \((x,y)\) 进行枚举,如果 \(scc[x]\neq scc[y]\),则在编号为 \(scc[x]\) 与编号为 \(scc[y]\) 的 SCC 连边,构成一张有向无环图,并把这张新图存下来,再进行一系列操作。
代码实现:
for(int i=1;i<=m;i++){
if(scc[x[i]]!=scc[y[i]]){
p[scc[x[i]]].push_back(scc[y[i]]);
}
}
例题:P3387 【模板】缩点,P2812 校园网络【[USACO]Network of Schools加强版】,P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G,T335409 银河
割点(割顶)
定义:对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点(又称割顶)。
判定:对于某个顶点 \(x\),如果存在至少一个顶点 \(y\)(\(x\) 的子节点),使得 \(low[y] \geq dfn[x]\),即不能回到祖先,那么 \(x\) 点为割点。
对于所有的根节点,则需要两个及以上的子节点,才能作为割点。
代码实现:
const int N=2e4+10,M=1e5+10;
vector<int> q[M];
int root;
int dfn[N],low[N],cut[N],t,cnt;
void tarjan(int x){
dfn[x]=low[x]=++t;
int child=0;
for(int i=0;i<q[x].size();i++){
int y=q[x][i];
if(!dfn[y]){
tarjan(y);
low[x]=min(low[x],low[y]);
if(low[y]>=dfn[x]){
child++;
if(x!=root||child>1) cut[x]=1;
}
}
else low[x]=min(low[x],dfn[y]);
}
}
int main(){
int n,m,a,b;
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>a>>b;
q[a].push_back(b);
q[b].push_back(a);
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
root=i;
tarjan(i);
}
}
以上的代码将所有的割点标记为了 \(1\)。注意每次做 \(Tarjan\) 的时候将 \(root\) 赋值为 \(i\)。
割边(桥)
定义:对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。
判定:与割点差不多,改成 \(low[y]>dfn[x]\) 就可以了,不需要考虑根节点的问题,但是需要判断不能重复经过已走路径(往回走)。
代码实现:
const int N=1e4+10;
vector<int> q[N];
int dfn[N],low[N],t,cnt,idx;
struct node{
int x,y;
}ans[N];
bool cmp(node m,node n){
if(m.x!=n.x) return m.x<n.x;
return m.y<n.y;
}
void tarjan(int x,int fa){
dfn[x]=low[x]=++t;
int child=0;
for(int i=0;i<q[x].size();i++){
int y=q[x][i];
if(!dfn[y]){
tarjan(y,x);
low[x]=min(low[x],low[y]);
if(low[y]>dfn[x]) ans[++idx].x=x,ans[idx].y=y;
}
else if(dfn[y]<dfn[u]&&y!=fa) low[x]=min(low[x],dfn[y]);
}
}
int main(){
int n,m,a,b;
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>a>>b;
q[a].push_back(b);
q[b].push_back(a);
}
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,-1);
sort(ans+1,ans+idx+1,cmp);
for(int i=1;i<=idx;i++) cout<<ans[i].x<<' '<<ans[i].y<<endl;
return 0;
}
程序输出即为所有割边。
例题:P1656 炸铁路
边双连通
定义:在一张连通的无向图中,对于两个点 \(x\) 和 \(y\),如果删去任意一条边都不能使它们不连通,我们就说 \(x\) 和 \(y\) 边双连通。
特征:具有传递性,若 \(x,y\) 边双连通,\(y,z\) 边双连通,则 \(x,z\) 边双连通。
边双连通分量 e-DCC
定义:对于一个无向图中的 极大 边双连通的子图,则称这个子图为一个 边双连通分量。
过程:与求强连通分量类似。
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10;
struct node{
int y,w;
};
vector<node> q[N];
vector<int> ans[N];
stack<int> s;
int dfn[N],low[N],scc[N],siz[N],t,cnt;
void tarjan(int x,int last){
dfn[x]=low[x]=++t;
s.push(x);
for(int i=0;i<q[x].size();i++){
int y=q[x][i].y,w=q[x][i].w;
if(w==(last^1)) continue;//成对变换(2^1=3,4^1=5...),避免走反边
if(!dfn[y]){
tarjan(y,w);
low[x]=min(low[x],low[y]);
}
else low[x]=min(low[x],dfn[y]);
}
if(dfn[x]==low[x]){
ans[++cnt].push_back(x);
while(1){
int y=s.top();
s.pop();
ans[cnt].push_back(y);
scc[y]=cnt;
siz[cnt]++;
if(y==x) break;
}
}
}
int main(){
int n,m,a,b;
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>a>>b;
q[a].push_back({b,i<<1});
q[b].push_back({a,i<<1|1});//给两条边建立成对下标,方便判重(见第16行)
}
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0);
cout<<cnt<<endl;
for(int i=1;i<=cnt;i++){
cout<<siz[i];
for(int j=0;j<siz[i];j++){
cout<<' '<<ans[i][j];
}
puts("");
}
return 0;
}
例题:P8436 【模板】边双连通分量,P2860 [USACO06JAN] Redundant Paths G
点双连通
定义:在一张连通的无向图中,对于两个点 \(x\) 和 \(y\),如果删去任意一个点(不包括 \(x,y\) 本身)都不能使它们不连通,我们就说 \(x\) 和 \(y\) 点双连通。
特征:不具有传递性,存在 \(x,y\) 点双连通,\(y,z\) 点双连通,而 \(x,z\) 不点双连通。
点双连通分量 v-DCC
定义:对于一个无向图中的 极大 点双连通的子图,我们称这个子图为一个 点双连通分量。
性质:
\(1\). 两个点双最多只有一个公共点,且一定是割点。
\(2\). 对于一个点双,它在 DFS 搜索树中 dfn 值最小的点一定是割点或者树根。
结论:
\(1\). 当这个点为割点时,它一定是点双连通分量的根,因为一旦包含它的父节点,他仍然是割点。
\(2\). 当这个点为树根时:有两个及以上子树,它是一个割点;只有一个子树,它是一个点双连通分量的根;它没有子树,视作一个点双。
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10,M=2e6+10;
stack<int> s;
vector<int> q[N],dcc[M];
int root,cut[N];
int dfn[N],low[N],scc[N],siz[N],t,cnt;
void record(int x,int y){
cnt++;
while(1){
int t=s.top();
s.pop();
dcc[cnt].push_back(t);
if(t==y) break;
}
dcc[cnt].push_back(x);
}
void tarjan(int x){
dfn[x]=low[x]=++t;
s.push(x);
if(x==root&&!q[x].size()){//孤点
dcc[++cnt].push_back(x);
return;
}
int child=0;
for(int i=0;i<q[x].size();i++){
int y=q[x][i];
if(!dfn[y]){
tarjan(y);
low[x]=min(low[x],low[y]);
if(low[y]>=dfn[x]){
child++;
if(child>1||x!=root) cut[x]=1;
record(x,y);
}
}
else low[x]=min(low[x],dfn[y]);
}
}
int main(){
int n,m,a,b;
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>a>>b;
if(a==b) continue;//重边
q[a].push_back(b);
q[b].push_back(a);
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
root=i;
tarjan(i);
}
}
cout<<cnt<<endl;
for(int i=1;i<=cnt;i++){
cout<<dcc[i].size();
for(int j=0;j<dcc[i].size();j++) cout<<' '<<dcc[i][j];
cout<<endl;
}
return 0;
}
对于代码中的第 \(34\) 行的 \(record\) 函数,在此特别进行说明。
已经判断出以 \(x\) 为根的子树 \(y\) 为一个 v-DCC ,此时要把这个子树存下来。
为什么只到节点 \(y\) 就 break 掉呢?因为 \(x\) 可能有多个子树,直接在栈里找到根 \(x\) 可能会把 \(x\) 的其他子节点存入,不能保证其正确性。所以此处存到 \(y\) 后就 break 掉,再存入根 \(x\)。

浙公网安备 33010602011771号