Tarjan详解
\(Tarjan\)
作用:
首先,我们要了解一个东西:强联通分量。
\(OI-Wiki\) 里说:
强连通的定义是:有向图 G 强连通是指,G 中任意两个结点连通。
强连通分量(Strongly Connected Components,SCC)的定义是:极大的强连通子图。
\(Tarjan\) 算法可以用来求强连通分量和缩点,同时,\(Tarjan\) 也可以用来求割点与桥及双联通分量,后文会讲。
有向图中:
强连通分量:
前置:
首先,我们要了解一个东西,叫做 DFS 生成树。
虽然我感觉这个东西不了解也行
我们以图为例:

图中分为 \(4\) 种边,分别是:
- 树边,也就是图中黑色的边,它是我们 DFS 遍历到的边,它们构成了一个 DFS 树。
- 返祖边,也就是图中红色的边,它是一个点连向其祖先的边。
- 横叉边,也就是图中蓝色的边,它是连接两个不同子树的的边。
- 前向边,也就是图中绿色的边,它是在搜索子树的过程中形成的。
那 DFS 生成树有什么用呢?
如果我们搜索到的点,是某个强连通分量在搜索树中遇到的第一个结点,那该强联通分量的其余节点一定在他的子树中。
过程:
\(Tarjan\) 算法主要是基于 DFS 进行运行的。
同时 \(Tarjan\) 维护了一个栈,用来维护还没有进入强连通分量的点。
\(Tarjan\) 针对每个点 \(i\) 定义了一些东西:
- \(dfn_i\),表示在 DFS 中是第几个被搜到的。\((dfs\) 序\()\)
- \(low_i\),表示在当前点的子树中,能够直接到达的在栈中且 \(dfn\) 值最小的那个点的 \(dfn\) 值。
注意:在有向图和无向图中,\(low\) 的定义不同。
\(Tarjan\) 的大体过程是这样的:
-
初始化当前点的 \(dfn\) 和 \(low\) ,将当前点入栈,标记当前点已经入栈。
-
遍历当前点 \(now\) 所有能够到达的点 \(i\),同时:
- 如果 \(i\) 还未被遍历,那 \(i\) 就是 \(now\) 的儿子,遍历 \(i\),通时根据定义使 \(low_{now}=\min(low_{now},low_i)\)。
- 如果 \(i\) 已经被遍历过了且还在栈中,那说明 \(i\) 和 \(now\) 处于同一强连通分量,使 \(low_{now}=\min(low_{now},dfn_i)\)。
-
遍历完后,如果 \(dfn_{now}=low_{now}\) 那说明子树中的点都比他大,也就是子树中的点无法到达比 \(now\) 还小的点,那此时栈中的点就是一个强联通分量。清除栈中点的标记并对它们染色。
如果没有看懂,想要例子,可以来这里,讲的例子的确很详细。
代码:
int dfn[1000010],low[1000010],vis[1000010],color[1000010];
int idx,n,m,sccsum;
vector<int>mp[1000010];//这里作者使用vector存的图,链式前向星也可以。
stack<int>s;
void TJ(int now){
//加入一个点
dfn[now]=low[now]=++idx; //初始化dfn和low
s.push(now); //入栈
vis[now]=1; //标记当前点在栈里
//搜索每个联通的点
for(int i:mp[now]){ //本句等价于for(int i=0;i<mp[now].size();i++),然后下文的i变为mp[now][i]即可
if(!dfn[i]){ //之前没访问过这个点
TJ(i); //访问
low[now]=min(low[now],low[i]); //其实这里也可以理解为让low[now]=当前强联通分量的"根"的dfn
}else if(vis[i]){ //之前访问过,并且在栈里
low[now]=min(low[now],dfn[i]); //说明访问到的点与现在这个点处于同一强联通分量
}
}
//判断是否是根
if(dfn[now]==low[now]){ //说明 现在这个点是根
sccsum++; //强联通分量的数量+1
int top; //栈顶元素
do{
top=s.top(); //取栈顶元素
vis[top]=0; //vis归零
s.pop(); //弹出
color[top]=sccsum; //染色
}while(now!=top);
}
}
习题:
\(B3609\) [图论与代数结构 701] 强连通分量 - 洛谷
题意及思路:
单纯板子题,不过要求按顺序输出强连通分量,那我们直接存到 vector 里,然后排个序即可。
代码:
//#pragma GCC optimize("O2")
#include<bits/stdc++.h>
using namespace std;
int dfn[100010],low[100010],vis[100010],sccsum,idx;
vector<int> color[100010];
vector<int>mp[100010];
stack<int>s;
struct node{
int id,fir;
bool operator <(const node &W)const{
return fir<W.fir;
}
}a[100010];
void tarjan(int now){
dfn[now]=low[now]=++idx;
s.push(now);
vis[now]=1;
for(int i:mp[now]){
if(!dfn[i]){
tarjan(i);
low[now]=min(low[now],low[i]);
}else if(vis[i]){
low[now]=min(low[now],dfn[i]);
}
}
if(dfn[now]==low[now]){
sccsum++;
int top;
do{
top=s.top();
s.pop();
vis[top]=0;
color[sccsum].push_back(top);
}while(now!=top);
}
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=m;i++){
int l,r;
cin>>l>>r;
mp[l].push_back(r);
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
tarjan(i);
}
}
cout<<sccsum<<"\n";
for(int i=1;i<=sccsum;i++){
sort(color[i].begin(),color[i].end());
a[i].id=i;
a[i].fir=color[i][0];
}
sort(a+1,a+sccsum+1);
for(int i=1;i<=sccsum;i++){
for(int j:color[a[i].id]){
cout<<j<<" ";
}
cout<<"\n";
}
return 0;
}
\(P2863\) [USACO06JAN] The Cow Prom S - 洛谷
题意及思路:
依旧模板题,要求输出大小大于一的强连通分量的个数,我们记一下每个强连通分量的大小即可。
代码:
//#pragma GCC optimize("O2")
#include<bits/stdc++.h>
using namespace std;
int dfn[100010],low[100010],vis[100010],sccsum,idx,ans;
vector<int> color[100010];
vector<int>mp[100010];
stack<int>s;
void tarjan(int now){
dfn[now]=low[now]=++idx;
s.push(now);
vis[now]=1;
for(int i:mp[now]){
if(!dfn[i]){
tarjan(i);
low[now]=min(low[now],low[i]);
}else if(vis[i]){
low[now]=min(low[now],dfn[i]);
}
}
if(dfn[now]==low[now]){
sccsum++;
int top;
do{
top=s.top();
s.pop();
vis[top]=0;
color[sccsum].push_back(top);
}while(now!=top);
}
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=m;i++){
int l,r;
cin>>l>>r;
mp[l].push_back(r);
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
tarjan(i);
}
}
for(int i=1;i<=sccsum;i++){
if(color[i].size()>1){
ans++;
}
}
cout<<ans;
return 0;
}
\(P1726\) 上白泽慧音 - 洛谷
题意及思路:
要求求出最大的强连通分量,我们用 vector 记一下即可。
代码:
//#pragma GCC optimize("O2")
#include<bits/stdc++.h>
using namespace std;
int dfn[100010],low[100010],vis[100010],sccsum,idx;
vector<int> color[100010];
vector<int>mp[100010];
stack<int>s;
struct node{
int id,fir;
bool operator <(const node &W)const{
return fir>W.fir;
}
}a[100010];
void tarjan(int now){
dfn[now]=low[now]=++idx;
s.push(now);
vis[now]=1;
for(int i:mp[now]){
if(!dfn[i]){
tarjan(i);
low[now]=min(low[now],low[i]);
}else if(vis[i]){
low[now]=min(low[now],dfn[i]);
}
}
if(dfn[now]==low[now]){
sccsum++;
int top;
do{
top=s.top();
s.pop();
vis[top]=0;
color[sccsum].push_back(top);
}while(now!=top);
}
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=m;i++){
int l,r,t;
cin>>l>>r>>t;
mp[l].push_back(r);
if(t==2){
mp[r].push_back(l);
}
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
tarjan(i);
}
}
for(int i=1;i<=sccsum;i++){
sort(color[i].begin(),color[i].end());
a[i].id=i;
a[i].fir=color[i].size();
}
sort(a+1,a+sccsum+1);
cout<<color[a[1].id].size()<<"\n";
for(int j:color[a[1].id]){
cout<<j<<" ";
}
return 0;
}
缩点:
缩点其实就是将每个强联通分量看成一个点,然后被缩点后的图一定是一个 \(DAG\) (拓扑图)。
然后就可以进行一些操作了。
缩点后要重新建图,代码如下:
for(int i=1;i<=n;i++){
mp[i].clear();
}
for(int i=1;i<=m;i++){//to存的是边
if(color[to[i].l]!=color[to[i].r]){
mp[color[to[i].l]].push_back(color[to[i].r]);
}
}
习题:
\(P2341\) [USACO03FALL / HAOI2006] 受欢迎的牛 G - 洛谷
题意及思路:
要求在缩点后求出该图唯一一个出度为 \(0\) 的强连通分量,若有多个或没有,输出 \(0\)。
那我们在缩点后计算出度,遍历所有强连通分量,查看是否正好有一个出度为 \(0\) 的强连通分量,最后输出即可。
代码:
//#pragma GCC optimize("O2")
#include<bits/stdc++.h>
using namespace std;
int dfn[1000010],low[1000010],vis[1000010],color[1000010],outsum[1000010],siz[1000010];
int idx,n,m,sccsum;
vector<int>mp[1000010];
stack<int>s;
void TJ(int now){
dfn[now]=low[now]=++idx;
s.push(now);
vis[now]=1;
for(int i:mp[now]){
if(!dfn[i]){
TJ(i);
low[now]=min(low[now],low[i]);
}else if(vis[i]){
low[now]=min(low[now],dfn[i]);
}
}
if(dfn[now]==low[now]){
sccsum++;
int top;
do{
siz[sccsum]++;
top=s.top();
vis[top]=0;
s.pop();
color[top]=sccsum;
}while(now!=top);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int l,r;
cin>>l>>r;
mp[l].push_back(r);
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
TJ(i);
}
}
for(int i=1;i<=n;i++){
for(int j:mp[i]){
if(color[i]!=color[j]){
outsum[color[i]]++;
}
}
}
int ans=0,sum=0;
for(int i=1;i<=sccsum;i++){
if(!outsum[i]){
ans=siz[i];
sum++;
}
}
if(sum==1){
cout<<ans;
}else{
cout<<0;
}
return 0;
}
无向图中:
无向图的 DFS 生成树:
以图为例:

图中有这两种边:
- 树边,即红色边,它是我们 DFS 遍历到的边,它们构成了一个 DFS 树。
- 返祖边,即蓝色边,它是一个点连向其祖先的边。
其实就是有向图中的那两种边的定义
\(low\)的定义:
在无向图中,\(low_i\) 代表 \(i\) 及其后代能到达的最小的 \(dfn\) 的值。
割边及边双连通,边双连通分量:
定义:
来自 \(OI-Wiki\):
对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。严谨来说,就是:假设有连通图 𝐺 ={𝑉,𝐸}
,𝑒
是其中一条边(即 𝑒 ∈𝐸
),如果 𝐺 −𝑒
是不连通的,则边 𝑒
是图 𝐺
的一条割边(桥)。
如图:

其中 \((2,1)\) 就是一条割边。
对于无向图中的一个点对 \((x,y)\),若删除 \((x,y)\) 之间的任意一个边后两点仍然连通,则称两点是边双连通的。
在一个边双连通分量 \(A\) 中,
- 任意取一个点对,它们都是边双连通的。
- 不存在 \(u \in G\) 且 \(u\notin A\) 使得 \(A \cup \left \{u\right \}\) 依然是一个边双连通分量。
求法:
一个点只会出现在一个边双连通分量里。
求割边和边双连通分量的大体过程如下:
-
初始化 \(dfn\) 和 \(low\) 数组。
-
遍历当前 \(now\) 能够到达的点 \(i\),
-
如果 \(i\) 是 \(now\) 儿子,则有: \(low_{now}=\min(low_{now},low_i)\)。
-
如果 \(i\) 被访问过了,但又不是 \(now\) 父亲,那该边是返祖边,则有: \(low_{now}=\min(low_{now},dfn_i)\)。
-
-
遍历完后,如果 \(dfn_{now}=low_{now}\),那说明 \(now\) 与其父亲的连边是一条割边,那此时栈中的点就是一个边双连通分量。清除栈中点的标记并对它们染色。
具体代码:
int dfn[100010],low[100010],vis[100010],sccsum,idx;
vector<int>color[100010];
struct node{
int id,to;
};
vector<node>mp[100010];
stack<int>s;
void tarjan(int now,int fa){
dfn[now]=low[now]=++idx;
s.push(now);
for(auto i:mp[now]){
if(!dfn[i.to]){
tarjan(i.to,i.id);
low[now]=min(low[now],low[i.to]);
}else{
if(i.id!=fa){
low[now]=min(low[now],dfn[i.to]);
}
}
}
//以下部分为求边双连通分量,若为求割边,改为标记即可
if(low[now]==dfn[now]){
sccsum++;
while(!s.empty()){
int top=s.top();
s.pop();
color[sccsum].push_back(top);
if(top==now){
break;
}
}
sort(color[sccsum].begin(),color[sccsum].end());
}
}
割点及点双连通,点双连通分量:
来自 \(OI-Wiki\):
对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点(又称割顶)。
如图:

图中 \(4\) 就是一个割点。
对于无向图中的一个点对 \((x,y)\),若删除 \((x,y)\) 之间的任意一个点后 \((\)不包括 \(x\)和\(y)\) 两点仍然连通,则称两点是点双连通的。
在一个点双连通分量 \(A\) 中,
- 任意取一个点对,它们都是点双连通的。
- 不存在 \(u \in G\) 且 \(u\notin A\) 使得 \(A \cup \left \{ u \right \}\) 依然是一个点双连通分量。

,𝑒
浙公网安备 33010602011771号