【学习笔记】tarjan 算法大杂烩
前置知识
啊嘿嘿,第三次学习 tarjan 终于是给我学明白了。
先来看一下 dfs 树上的三种边:
- 树边:从父亲连向儿子的边
- 返祖边:从儿子连向祖先的边
- 横叉边:除了前两种边之外的边
tarjan 算法中常用的两个数组:
- \(dfn_i\):表示 \(i\) 的时间戳,即第几个被遍历到
- \(low_i\):表示 \(i\) 只经过最多一条返祖边到达的最小时间戳
下面正片开始!
有向图的 tarjan 算法
求强连通分量
这个好像是 tarjan 算法在有向图中的唯一常见应用吧~
强连通分量定义
对于有向图中的一个联通子图 \(G(V,E)\),若子图中的任意两点之间都是可以到达的,则称 \(G(V,E)\) 是一个强连通分量
实现
对着代码理解吧~
dfn[u] = low[u] = ++tim
初始化:显然一个点不经过任何边可以到达的最小时间戳是他自己
if(!dfn[v]){
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if(in[v]) low[u] = min(low[u], dfn[v]);
如果当前点的时间戳没有被算过就说明是树边,否则就是非树边。
对于一条非树边只能用 \(v\) 的 \(dfn\) 去更新当前的 \(low\),否则可以用 \(low\) 来更新当前点的 \(low\)。
注意,有向图中的 dfs 树可能会出现横叉边的情况,而横叉边是不可以用来更新 \(low\) 的,所以要特判(用是否在栈内,即是否属于当前子树来判断)。
if(low[u] == dfn[u]){
++tt;
int v = st.top();
while(v != u){
belong[v] = tt, in[v] = 0;
scc[tt].pb(v);
st.pop(), v = st.top();
}
belong[v] = tt, in[v] = 0;
scc[tt].pb(v);
st.pop();
}
如果遇到 \(dfn\) 与 \(low\) 相等的情况,说明它的子树内还留在栈内的点最靠上就到它了(到不了它的已经被弹掉)。
因此当前点可以到留在栈内的所有点(通过树边),而栈内的所有点都可以到它,所以这些点可以以当前点为中枢,两两联通。
所以它的子树内还留在栈内的点了,与它构成一个极大强连通分量。
code
#include<bits/stdc++.h>
#define pb push_back
#define N 10010
#define Fo(a, b) for(auto a : b)
#define fo(a, b, c) for(int b = a; b <= c; b++)
using namespace std;
int n, m, dfn[N], low[N], tim, in[N], vis[N], tt, belong[N];
stack<int>st;
vector<int>G[N], scc[N];
void tarjan(int u){
dfn[u] = low[u] = ++tim, in[u] = 1;
st.push(u);
Fo(v, G[u]){
if(!dfn[v]){
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if(in[v]) low[u] = min(low[u], dfn[v]);
}
if(low[u] == dfn[u]){
++tt;
int v = st.top();
while(v != u){
belong[v] = tt, in[v] = 0;
scc[tt].pb(v);
st.pop(), v = st.top();
}
belong[v] = tt, in[v] = 0;
scc[tt].pb(v);
st.pop();
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m;
fo(1, i, m){
int x, y; cin >> x >> y;
G[x].pb(y);
}
fo(1, i, n) if(!dfn[i]) tarjan(i);
cout << tt << "\n";
fo(1, i, n){
if(vis[belong[i]]) continue;
int u = belong[i];
vis[u] = 1;
sort(scc[u].begin(), scc[u].end());
Fo(v, scc[u]) cout << v << ' ';
cout << "\n";
}
return 0;
}
应用:缩点
先跑一边 tarjan 求强连通分量,然后将每个强连通分量视为一个点。
将缩点后的不同新点之间连边,建一张新图。
code
#include<bits/stdc++.h>
#define maxn 100010
using namespace std;
int n, m;
struct edge{
int to, next;
}e[maxn << 1], e1[maxn << 1];
int cnt = 0, a[maxn], head[maxn];
int cnt1 = 0, head1[maxn], ind[maxn], w[maxn];
int tim = 0, low[maxn], tot = 0, dfn[maxn], belong[maxn], d[maxn];
bool vis[maxn];
stack<int>st;
vector<int>scc[maxn];
void add(int x, int y){
e[++cnt].to = y;
e[cnt].next = head[x];
head[x] = cnt;
}
void add1(int x, int y){
e1[++cnt1].to = y;
e1[cnt1].next = head1[x];
head1[x] = cnt1;
}
void tarjan(int u){
dfn[u] = ++tim;
low[u] = tim;
vis[u] = 1;
st.push(u);
for(int i = head[u]; i; i = e[i].next){
int v = e[i].to;
if(!dfn[v]){
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if(vis[v]) low[u] = min(low[u], dfn[v]);
}
if(dfn[u] == low[u]){
++tot;
while(st.top() != u){
int x = st.top();
vis[x] = 0;
belong[x] = tot;
w[tot] += a[x];
scc[tot].push_back(x);
st.pop();
}
st.pop();
vis[u] = 0;
belong[u] = tot;
w[tot] += a[u];
scc[tot].push_back(u);
}
}
void topo(){
queue<int>q;
for(int i = 1; i <= n; i++){
if(!ind[i]){
q.push(i);
d[i] = w[i];
}
}
while(!q.empty()){
int u = q.front();
q.pop();
for(int i = head1[u]; i; i = e1[i].next){
int v = e1[i].to;
d[v] = max(d[v], d[u] + w[v]);
ind[v]--;
if(!ind[v]) q.push(v);
}
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> a[i];
}
for(int i = 1; i <= m; i++){
int x, y;
cin >> x >> y;
add(x, y);
}
for(int i = 1; i <= n; i++){
if(!dfn[i]) tarjan(i);
}
for(int u = 1; u <= n; u++){
for(int i = head[u]; i; i = e[i].next){
int v = e[i].to;
int x = belong[u], y = belong[v];
if(belong[u] != belong[v]){
add1(x, y);
ind[y]++;
}
}
}
topo();
int ans = 0;
for(int i = 1; i <= tot; i++){
ans = max(ans, d[i]);
}
cout << ans;
return 0;
}
无向图的 tarjan 算法
注意!!,由于无向图的 dfs 树不存在横叉边,所以所有的无向图的 tarjan 算法都不需要判是否在栈内。
求割点
割点定义
通俗的理解为,去掉一个点后,整张无向图就不再联通,那么这个点就可以称为该图的一个割点。
实现
与求强连通分量类似。
注意:
if(low[v] >= dfn[u]) flag = 1;
是大于等于号,不是大于号。并且不能直接将推进去,而需要一个 flag 标记,不然会将某些点算重
if(u == rt){
if(son > 1) ans.pb(rt);
}
如果当前点是 dfs 树的根,需要判断它在 dfs 树上是否有两个及以上的儿子。
如果它只有一个儿子,那么去掉它后整张图仍然还是联通的。
code
#include<bits/stdc++.h>
#define fo(a, b, c) for(int b = a; b <= c; b++)
#define Fo(a, b) for(auto a : b)
#define pb push_back
#define N 100010
using namespace std;
int n, m, tim, dfn[N], low[N], rt;
vector<int>G[N], ans;
void tarjan(int u){
dfn[u] = low[u] = ++tim;
int son = 0, flag = 0;
Fo(v, G[u]){
if(!dfn[v]){
++son;
tarjan(v);
low[u] = min(low[u], low[v]);
if(low[v] >= dfn[u]) flag = 1;
}
else low[u] = min(low[u], dfn[v]);
}
if(flag){
if(u == rt){
if(son > 1) ans.pb(rt);
}
else ans.pb(u);
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m;
fo(1, i, m){
int x, y; cin >> x >> y;
G[x].pb(y), G[y].pb(x);
}
fo(1, i, n) if(!dfn[i]) rt = i, tarjan(i);
cout << ans.size() << "\n";
sort(ans.begin(), ans.end());
Fo(u, ans) cout << u << ' ';
return 0;
}
应用:求点双连通分量
点双定义:
若去掉任意一个点后整张图仍然还是连通的,则这个图是一个点双连通分量。
实现
注意到每个点至少属于一个点双连通分量,且一个割点可能属于多个点双。
所以可以先将割点求出来,每个割点下边必然挂了至少一个点双。
但是注意,这里并不需要判断根节点是否有两个以上的儿子,如果根节点只有一个儿子,则他下边也会挂一个点双。
最后不要忘了判孤立的点(也是点双)。
注意,\(u,v\) 之间可能有别的点留着,所以不能弹到 \(u\) 停,而是要弹到 \(v\) 停。
code
#include<bits/stdc++.h>
#define N 100010
#define fo(a, b, c) for(int b = a; b <= c; b++)
#define Fo(a, b) for(auto a : b)
#define pb push_back
using namespace std;
int n, m, dfn[N], low[N], tim, tt, rt;
vector<int>G[N], scc[N];
stack<int>st;
void tarjan(int u){
dfn[u] = low[u] = ++tim;
st.push(u);
int son = 0;
Fo(v, G[u]){
if(!dfn[v]){
++son;
tarjan(v);
low[u] = min(low[u], low[v]);
if(low[v] >= dfn[u]){
++tt;
int x = st.top();
while(x != v){
scc[tt].pb(x);
st.pop(), x = st.top();
}
st.pop();
scc[tt].pb(v), scc[tt].pb(u);
}
}
else low[u] = min(low[u], dfn[v]);
}
if(!son && u == rt) scc[++tt].pb(u);
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m;
fo(1, i, m){
int x, y; cin >> x >> y;
G[x].pb(y), G[y].pb(x);
}
fo(1, i, n) if(!dfn[i]){
while(!st.empty()) st.pop();
rt = i, tarjan(i);
}
cout << tt << "\n";
fo(1, i, tt){
cout << scc[i].size() << ' ';
Fo(x, scc[i]) cout << x << ' ';
cout << "\n";
}
return 0;
}
求割边
割边定义
与割点类似,可以理解为,去掉一条边后,整张无向图就不再联通,那么这条边就可以称为该图的一个割边。
实现
与求割边类似。
注意:
if(low[v] > dfn[u]) e[id] = 1;
是大于号,不是大于等于号。
else if(id != fa) low[u] = min(low[u], dfn[v]);
需要记录来时边的编号,只能用非反向边的边来更新
code
#include<bits/stdc++.h>
#define N 2000010
#define pb push_back
#define fo(a, b, c) for(int b = a; b <= c; b++)
#define Fo(a, b) for(auto a : b)
using namespace std;
int n, m, dfn[N], low[N], tim;
int e[N], tt, vis[N];
struct edge{
int id, v;
};
vector<edge>G[N];
vector<int>scc[N];
void tarjan(int u, int fa){
dfn[u] = low[u] = ++tim;
Fo(to, G[u]){
int id = to.id, v = to.v;
if(!dfn[v]){
tarjan(v, id);
low[u] = min(low[u], low[v]);
if(low[v] > dfn[u]) e[id] = 1;
}
else if(id != fa) low[u] = min(low[u], dfn[v]);
}
}
void dfs(int u){
scc[tt].pb(u), vis[u] = 1;
Fo(to, G[u]){
int v = to.v, id = to.id;
if(e[id] || vis[v]) continue;
dfs(v);
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n >> m;
fo(1, i, m){
int x, y; cin >> x >> y;
G[x].pb({i, y}), G[y].pb({i, x});
}
fo(1, i, n) if(!dfn[i]) tarjan(i, 0);
fo(1, i, n) if(!vis[i]){
++tt, dfs(i);
}
cout << tt << "\n";
fo(1, i, tt){
cout << scc[i].size() << ' ';
Fo(x, scc[i]) cout << x << ' ';
cout << "\n";
}
return 0;
}
应用:求边双连通分量
定义
去掉任意一条边后,任然能保持连通的图。
实现
先把所有割边求出来去掉,然后跑 dfs,每个连通块就是一个边双连通分量。
code
放上边了。QAQ

浙公网安备 33010602011771号