Tarjan连通算法
Tarjan 算法是基于深度优先搜索的算法,用于求解图的连通性问题。Tarjan 算法可以在线性时间内求出无向图的割点与桥,进一步地可以求解无向图的双连通分量;同时,也可以求解有向图的强连通分量、必经点与必经边。
如果你对上面的一些术语不是很了解,没关系,我们只要知道 Tarjan 算法是基于深度优先搜索的,用于求解图的连通性问题的算法 就好了。
提到 Tarjan,不得不提的就是算法的作者 ——Robert Tarjan。他是一名著名的计算机科学家,我们耳熟能详的 最近公共祖先(LCA)问题、强连通分量 问题、双连通分量 问题的高效算法都是由他发现并解决的,同时他还参与了开发斐波那契堆、伸展树 的工作。
时间戳与追溯值
在对一个图进行DFS的过程中,每一个节点都会被访问一次,而且,节点的访问是有线性顺序的,我们把这个线性顺序称为时间戳。设节点为 v v v,时间戳记为 v . d v.d v.d
对于追溯值 v . l o w v.low v.low,我们有如下数学定义:
v . l o w = min ( v . d , w . d ) v.low = \min(v.d,w.d) v.low=min(v.d,w.d)
其中节点 v v v存在一个后代节点 u u u(包括节点 v v v自己),存在一条后向边 u → w u \to w u→w。
例如下图:DFS访问顺序为 1 , 2 , 3 1,2,3 1,2,3,到 3 3 3的时候,发现有一条后向边 3 → 1 3 \to 1 3→1。那么:
节点编号 | 时间戳 | 追溯值 |
---|---|---|
1 | 1 | 1 |
2 | 2 | 1 |
3 | 3 | 1 |
可以理解追溯值为一个环的入口。
线性时间求时间戳和追溯值
在线性时间求时间戳和追溯值是Tarjan算法的核心思想,其原理还是DFS,大致伪代码如下:
Tarjan(u):
设置节点u的时间戳;
u.low = u.d;
依次访问从u出发的每一个边(u,v):
如果v没有被访问:
Tarjan(v);
u.low = min(u.low,v.low);
如果节点v在当前DFS过程中,即产生了一条后向边:
u.low = min(u.low,v.d);
有时候图并不连通,所以要在一个循环里面使用Tarjan算法。
注意,tarjan算法给强量筒分量标号的顺序是按照拓扑序的反序进行标号。
Tarjan算法的应用
求强连通分量&分量图&缩点
大致伪代码如下:
Tarjan(u):
设置节点u的时间戳;
u.low = u.d;
将节点u入栈
依次访问从u出发的每一个边(u,v):
如果v没有被访问:
Tarjan(v);
u.low = min(u.low,v.low);
如果节点v在当前DFS过程中(v一定在栈中),即产生了一条后向边:
u.low = min(u.low,v.d);
如果 u.d = u.low(说明u是环的入口):
一直弹出栈,直到栈顶是u为止,最后弹出栈顶元素u,所有被弹出的元素都在一个环中,即都在同一个强连通分量中
例题:P3387 缩点
#include <bits/stdc++.h>
using namespace std;
#define FR freopen("in.txt","r",stdin)
typedef long long ll;
struct Edge
{
int from;
int to;
int nxt;
} e[100005],eb[100005];
int head[10005];
int headb[10005];
int wei[10005];
int tot = 0;
int totb = 0;
int ans[10005];
int scc[10005];
int low[10005];
int dfn[10005];
int scci = 0;
int weib[10005];
inline void connect(int u,int v)
{
tot++;
e[tot].from = u;
e[tot].to = v;
e[tot].nxt = head[u];
head[u] = tot;
}
inline void connectb(int u,int v)
{
totb++;
eb[totb].from = u;
eb[totb].to = v;
eb[totb].nxt = headb[u];
headb[u] = totb;
}
int n,m;
stack<int> sta;
int di = 0;
bool k[10005];
void tarjan(int idx)
{
dfn[idx] = low[idx] = ++di;
sta.push(idx);
k[idx] = true;
for(int ne = head[idx]; ne != 0; ne = e[ne].nxt)
{
if(dfn[e[ne].to] == 0)
{
tarjan(e[ne].to);
low[idx] = min(low[idx],low[e[ne].to]);
}
else if(k[e[ne].to])
{
// back edge
low[idx] = min(low[idx],dfn[e[ne].to]);
}
}
if(dfn[idx] == low[idx])
{
scci++;
// scc
while(1)
{
int curr = sta.top();
sta.pop();
k[curr] = false;
scc[curr] = scci;
weib[scci]+= wei[curr];
ans[scci] += wei[curr];
if(curr == idx)
{
break;
}
}
}
}
stack<int> topic;
bool vis[10005];
void dfs(int u)
{
vis[u] = true;
for(int ne = headb[u]; ne != 0; ne = eb[ne].nxt)
{
if(!vis[eb[ne].to])
{
dfs(eb[ne].to);
}
}
topic.push(u);
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
cin >> wei[i];
}
while(m--)
{
int u,v;
cin >> u >> v;
connect(u,v);
}
for(int i = 1; i<=n; i++)
{
if(dfn[i] == 0)
{
tarjan(i);
}
}
for(int i = 1; i<=tot; i++)
{
if(scc[e[i].from] != scc[e[i].to])
{
connectb(scc[e[i].from],scc[e[i].to]);
}
}
for(int i = 1; i<=scci; i++)
{
if(!vis[i])
{
dfs(i);
}
}
int aanns = 0;
while(!topic.empty())
{
int u = topic.top();
topic.pop();
aanns = max(aanns,ans[u]);
for(int ne = headb[u]; ne != 0; ne = eb[ne].nxt)
{
ans[eb[ne].to] = max(ans[u] + weib[eb[ne].to],ans[eb[ne].to]);
}
}
cout << aanns;
return 0;
}
求割点
一个点是不是割点,主要判断一下几个条件:
- 如果节点 v v v是DFS树的根节点,如果 v v v有至少两个子节点,那么 v v v是割点。
- 如果节点
v
v
v不是DFS树的根节点,但是
v
v
v存在一颗子树,这个子树中的任何节点都没有后向边指向
v
v
v的祖先节点,那么
v
v
v是割点。
#include <bits/stdc++.h>
using namespace std;
#define FR freopen("in.txt","r",stdin)
typedef long long ll;
struct Edge
{
int to;
int nxt;
};
int n,m;
struct Graph
{
Edge e[200010];
int head[30010];
int tot;
Graph()
{
tot = 0;
for(int i = 1; i <= n; i++)
head[i] = 0;
}
void add(int u,int v)
{
tot++;
e[tot].to = v;
e[tot].nxt = head[u];
head[u] = tot;
}
} g;
ll dfn[30010],low[30010],ffn[30010];
ll id;
int ans[30010];
int cc = 0;
void tarjan(int u,bool root)
{
dfn[u] = low[u] = ++id;
bool ready = false;
int cnt = 0;
for(int ne = g.head[u]; ne != 0; ne = g.e[ne].nxt)
{
if(dfn[g.e[ne].to] == 0)
{
tarjan(g.e[ne].to,false);
low[u] = min(low[u],low[g.e[ne].to]);
if(low[g.e[ne].to] >= dfn[u])
{
ready = true;
}
cnt++;
}
else if(ffn[g.e[ne].to] == 0)
{
low[u] = min(low[u],dfn[g.e[ne].to]);
}
}
ffn[u] = ++id;
if((ready && !root) || (root && cnt > 1))
{
ans[cc++] = u;
}
}
int main()
{
cin >> n >> m;
for(int i = 1; i<=m; i++)
{
int u,v;
cin >> u >> v;
g.add(u,v);
g.add(v,u);
}
for(int i = 1; i<=n; i++)
{
if(dfn[i] == 0)
{
tarjan(i,true);
}
}
cout << cc << endl;
sort(ans,ans+cc);
copy(ans,ans+cc,ostream_iterator<int>(cout," "));
return 0;
}