Tarjan算法小结
作用
-
求有向图强连通分量
-
求无向图割点
-
求无向图桥边
-
求无向图点双连通分量
-
求无向图边双连通分量
-
有向图强连通分量缩点
-
无向图双连通分量缩点
-
求树节点最近公共祖先
其中,前七种是同一个算法的变形,第八种则是同名别种算法(同样由Tarjan同志开发)。八种算法都是基于深度优先遍历,复杂度都为 O( n n n)
思路
求强连通分量
一次DFS,设置两个数组,一个是时间戳(我写的timestamp,因为工程上基本都叫这个,然而我看到别人的教程基本用的是dfn),一个是low(就是储存本节点能抵达的最远的祖先的时间戳),时间戳就是DFS访问的时间,就是到达各个点的DFS序。每一次访问未访问过的节点,就给它赋值上当前的时间戳,并且把low设置为和时间戳相等,然后从它的子节点中选择最小的可到达时间戳进行更新,因为它的子节点能到达的,它肯定也能通过该子节点到达。求强连通分量的时候我们还会用到一个栈,大家都知道DFS的本质其实是用栈来搜索(相应的,BFS则是用队列),现在我们用这个栈的目的是存储强连通分量的各个点的编号。当一个点的low和时间戳在遍历过它所有的子节点之后仍然相等的话,说明它本身是该分量中能访问到的最早的节点,也就是说,栈内从栈顶到他自己(也就是在它之后访问的所有节点)都在同一个强连通分量中不能再访问其他节点了,这时候把它和它之上的所有节点都出栈我们就得到了一个强连通分量。然后返回它的父节点继续向下遍历。这样之后,所有的点实际上都只遍历了一次,算法时间复杂度只有O( n n n).
代码
改修版 CF 475B AC确认,该题图有点奇怪,而且数据小暴搜可过,此处为通用版
#include<iostream>
#include <stack>
#include <vector>
#include <cstring>
using namespace std;
/*
潘骏同志自己瞎几把模拟的Tarjan算法
本板子是求强连通分量的
*/
bool vis[1005];
stack<int> ss;
int timestamp[1005];
int low[1005];
int now;
struct node
{
int ind;
vector<int> sons;
}mapper[1005];
void tarjan(int num)
{
now++;//更新最新时间戳
timestamp[num]=now;
low[num]=now;//初始条件自己能返回的最近的节点就是自己,所以保持low和时间戳一致。
vis[num]=true;//标记本节点入栈。
ss.push(num);
for(int i=0;i<mapper[num].sons.size();i++)
{
int temp=mapper[num].sons[i];
if(timestamp[temp]==0)//vis是用来判断在不在栈里的,要判断搜索过没有就需要用时间戳判断了。
{
tarjan(temp);
if(low[temp]<low[num])
{
low[num]=low[temp];//如果子节点能够追溯到比自己更早的节点,那么更新自己能到达的最早节点为子节点的low
}
}
else
{
if(vis[temp] && low[temp]<low[num])
{
low[num]=low[temp];//大佬们写的都是用时间戳更新,据说是找桥的时候避免上翻过多,但是这里是找强连通分量,那就没关系。
}
}
}
if(timestamp[num]==low[num])//如果自己所能追溯到的最早节点始终是自己,那么它就是本次搜索中强连通分量的入口,所在的强连通分量已经全部被遍历完毕,把自己开始的所有栈顶元素出栈即可。
{
while(ss.top()!=num)
{
int temp=ss.top();
cout<<temp<<" ";
vis[temp]=false;
ss.pop();
}
cout<<num<<"\n";
ss.pop();
vis[num]=false;
}
}
int main()
{
int n,m;
cin>>n>>m;
for(int i=0;i<m;i++)
{
int a,b;
cin>>a>>b;
mapper[a].sons.push_back(b);
}
memset(timestamp,0,sizeof(timestamp));
memset(vis,0,sizeof vis);
memset(low,0,sizeof(low));
now=0;
tarjan(1);
return 0;
}
求割点
如果是在无向图中求割点或者桥,那么我们需要加一个限制,就是不能原路返回 所以我们记录每个节点在DFS树上的父亲。
如果子节点的最小可到达节点小于自己,那么说明下面的点都不能通过这条边以外的其他边回到本节点,于是即可得出该点摘了/该边砍了就肯定影响全图联通性的结论,该点/该边即为割点/桥边。看书很重要,请看下述定理,摘自刘汝佳蓝书。
定理(刘汝佳、陈锋《算法竞赛入门经典训练指南》P313) :
在无向连通图G的DFS树中,非根节点u是G的割顶当且仅当u存在一个子节点v,使得v及其后代都没有反向边连回u的祖先(连回u不算)
代码
改修版 UVA 315 AC确认,该题输入比较毒瘤,此处为通用版
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
/*
潘骏同志自己瞎几把模拟的Tarjan算法
本板子是求割点的
*/
using namespace std;
vector<int> egs[105];
int timestamp[105];
int low[105];//此处low和timestamp数组和上文所述的作用一样
bool iscut[105];//标记割点的数组
int now;
void tarjan(int num,int fa)
{
now++;
timestamp[num]=now;
low[num]=now;
int children=0;
for(int i=0;i<egs[num].size();i++)
{
int next=egs[num][i];
if(timestamp[next]==0)
{
children++;//仅用于根节点数由树边相连的儿子节点个数。
tarjan(next,num);
if(low[next]<low[num])
{
low[num]=low[next];
}
else if(low[next]>=timestamp[num])
{
iscut[num]=true;//注意此定理仅对非根节点有效。
}
}
else
{
if(timestamp[temp]<timestamp[num]&&fa!=next)//禁止原路返回
{
low[num]=min(low[num],timestamp[next]);
}
}
}
if(timestamp[num]==1 && children<=1)
{//因为上述定理仅对非根节点有效,所以我们需要对根进行特判,只需要数孩子数即可。
iscut[num]=false;
}
}
int main() {
int n,m;
while(cin>>n>>m)
{
memset(timestamp,0,sizeof timestamp);
memset(low,0,sizeof low);
memset(iscut,0,sizeof iscut);
now=0;
for(int i=1;i<=n;i++)
{
egs[i].clear();
}
for(int i=0;i<m;i++)
{
int a,b;
cin>>a>>b;
egs[a].push_back(b);
egs[b].push_back(a);
}
tarjan(1,0);
int ans=0;
for(int i=1;i<=n;i++)
{
if(iscut[i])
{
ans++;
}
}
cout<<ans<<endl;
}
return 0;
}
求桥边
在求割点的过程中,作为一种特殊情况,如果某个节点的后代只能连回该点自己,只要删除一条连向该点的边,就可以让图非连通了。所以只需要少许修改上述求割点的代码即可获得求桥边的代码。
代码
改修版 HDU 4738 通过,该题需要判重边和一些特殊毒瘤判断,此处为通用版
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <stack>
#include <set>
/*
潘骏同志自己瞎几把模拟的Tarjan算法
本板子是求桥的
*/
using namespace std;
int timestamp[10005];
int low[10005];//此处low和timestamp数组和上文所述的作用一样
int same[10005][10005];//去重用,数据如果太大的话可能需要别的办法
int now;
struct edge
{
int from;
int to;
int val;//如果没有边权可以不要
bool operator < (const edge & b)const {
if(val!=b.val)
{
return val<b.val;
} else if(from != b.from)
{
return from<b.from;
}
else
{
return to<b.to;
}
}
};
vector<edge> egs[10005];
set<edge> bridges;//存储桥边
void tarjan(int num,int fa)
{
now++;
timestamp[num]=now;
low[num]=now;
for(int i=0;i<egs[num].size();i++)
{
edge now=egs[num][i];
int next=now.to;
if(timestamp[next]==0)
{
tarjan(next,num);
if(low[next]<low[num])
{
low[num]=low[next];
}
else if(low[next]>timestamp[num])//注意不能等于
{
if(same[num][next]!=2) {//如果有重边则改边删除不能改边连通情况,故不记录
bridges.insert(now);
}
}
}
else
{
if(fa!=next)
{
low[num]=min(low[num],timestamp[next]);
}
}
}
}
int main()
{
int n,m;
while(cin>>n>>m,n!=0) {
memset(timestamp,0,sizeof timestamp);
memset(low,0,sizeof low);
memset(same,0,sizeof same);
for(int i=1;i<=n;i++)
{
egs[i].clear();
}
bridges.clear();
now=0;
for (int i = 0; i < m; i++) {
int u,v,w;
cin>>u>>v>>w;//w是边权,题目不要求的话可以不要
edge temp;
if(!same[u][v]) {
temp.val = w;
temp.from = u;
temp.to = v;
egs[u].push_back(temp);
temp.to = u;
temp.from = v;
egs[v].push_back(temp);
same[u][v]=1;
same[v][u]=1;
} else
{
same[u][v]=2;
same[v][u]=2;
}
}
tarjan(1,0);
cout<<bridges.size()<<endl;
}
return 0;
}
求无向图点双连通分量
对于一个无向连通图,如果任意两个点之间存在两种点不重复的路径,则说明改图是点双连通的。一般双连通都是指点双连通,边的双连通会额外说明。
对于一张无向图,点双连通的极大子图称为双连通分量或块。不同的双连通块之间最多有一个公共点,该公共点就是我们之前求过的割点。任意割点都是双连通块之间的公共点。但是双连通块之间的边是不重复的。
代码
改修版 UVA LIVE 5135 AC确认,该题要求尽量不选择割点的情况下在双连通分量中选两个点涂黑,此处为通用版
#include<iostream>
#include <stack>
#include <vector>
#include <cstring>
#include <set>
using namespace std;
long long timestamp[50005];
long long low[50005];
bool iscut[500005];//存储割点
long long now;
vector<long long> egs[50005];
struct edge
{
long long from;
long long to;
edge(long long f,long long t):from(f),to(t){
}
};
long long bcc[50005];//染色,但是割点的颜色是没有意义的,因为它同时属于两个双连通分量却只有一种颜色
vector<long long> bc[50005];//分别存储在同一双连通分量内的点
long long bccounter;
stack<edge> ss;//本栈存的是边,因为点双连通分量是没有重复边的。
void tarjan(long long num,long long fa)
{
now++;
timestamp[num]=now;
low[num]=now;
long long children=0;
for(long long i=0; i<egs[num].size(); i++)
{
long long next=egs[num][i];
if(timestamp[next]==0)
{
children++;
ss.push(edge(num,next));
tarjan(next,num);
if(low[next]<low[num])
{
low[num]=low[next];
}
else if(low[next]>=timestamp[num])
{
iscut[num]=true;
bccounter++;//找到了新的双连通分量
bc[bccounter].clear();//初始化新的双连通分量
while(1)
{
edge nowp=ss.top();
if(bcc[nowp.from]!=bccounter) {
bc[bccounter].push_back(nowp.from);
bcc[nowp.from]=bccounter;
}
if(bcc[nowp.to]!=bccounter){
bc[bccounter].push_back(nowp.to);
bcc[nowp.to]=bccounter;
}
//出栈染色
ss.pop();
if(nowp.from==num && nowp.to==next)
{
//出栈完成
break;
}
}
}
}
else
{
if(timestamp[next]<timestamp[num] && fa!=next)//禁止原路返回
{
ss.push(edge(num,next));
low[num]=min(low[num],timestamp[next]);
}
}
}
if(timestamp[num]==1 && children<=1)
{
iscut[num]=false;
}
}
int main()
{
long long n,m;
while(cin>>n>>m && m!=0)
{
memset(iscut,0,sizeof iscut);
bccounter=0;
memset(bcc,0,sizeof(bcc));
memset(timestamp,0,sizeof(timestamp));
memset(low,0,sizeof(low));
now=0;
for(long long i=1;i<=n;i++)
{
egs[i].clear();
}
for(long long i=0; i<m; i++)
{
long long a,b;
cin>>a>>b;
egs[a].push_back(b);
egs[b].push_back(a);
}
tarjan(1,0);
for(long long i=1;i<=bccounter;i++)
{
cout<<"bcc #"<<i<<": ";
for(long long j=0;j<bc[i].size();j++)
{
cout<<bc[i][j]<<" ";
}
cout<<endl;
}
}
return 0;
}
求无向图边双连通分量
这个比刚才的点双连通分量简单多了,只需要low值(即能到达的最远祖先点)相同,就能判断两个点在同一个边双连通分量里
代码
改修版 POJ 3177 AC确认,该题是求双连通分量度数为1(叶子)的全都加一条边连即可,此处为通用版
#include <iostream>
#include <vector>
#include <cstring>
#include <set>
using namespace std;
int timestamp[5005];
int low[5005];
int now;
vector<int> egs[5005];
set<int> ebcc[5005];
bool same[5005][5005];
void tarjan(int num,int fa)
{
now++;
timestamp[num]=now;
low[num]=now;
for(int i=0;i<egs[num].size();i++)
{
int next=egs[num][i];
if(timestamp[next]==0)
{
tarjan(next,num);
if(low[next]<low[num])
{
low[num]=low[next];
}
} else
{
if(timestamp[next]<timestamp[num]&&fa!=next)
{
low[num]=min(timestamp[next],low[num]);
}
}
}
}
int main() {
int n,m;
while(cin>>n>>m)
{
memset(timestamp,0,sizeof timestamp);
memset(low,0,sizeof timestamp);
memset(same,0,sizeof same);
for(int i=1;i<=n;i++)
{
egs[i].clear();
ebcc[i].clear();
}
now=0;
for(int i=0;i<m;i++)
{
int u,v;
cin>>u>>v;
if(!same[u][v]) {//去重边(POJ 3177和POJ 3352唯一的区别)
egs[u].push_back(v);
egs[v].push_back(u);
same[u][v]=true;
same[v][u]=true;
}
}
tarjan(1,0);
for(int i=1;i<=n;i++)
{
for(int j=0;j<egs[i].size();j++)
{
int temp=egs[i][j];
ebcc[low[temp]].insert(temp);
//因为反正是遍历的,另外一点迟早还得再来一次所以只考虑一边。
}
}
int ebccnt=0;
for(int i=1;i<=n;i++)
{
if(!ebcc[i].empty())
{
ebccnt++;
cout<<"ebcc #"<<ebccnt<<": ";
for(auto j:ebcc[i])
{
cout<<j<<" ";
}
cout<<endl;
}
}
}
return 0;
}
缩点
比较简便的方法是记录各自的强联通分量的编号然后重新建立新图,一般不卡内存就能用,没什么题目会卡内存吧。。。
代码
HDU 5934 AC确认,此处为通用版,该题和计算几何结合
#include <iostream>
#include <stack>
#include <vector>
#include <algorithm>
#include <unordered_map>
/*
* 本版子为潘骏同志tarjan强联通缩点板子
*/
using namespace std;
bool vis[1005];
stack<long long> ss;
long long timestamp[1005];
long long low[1005];
long long now;
vector<long long> egs[1005];
long long scc[1005];
unordered_map<int,bool> mapper[1005];//新图邻接矩阵表
vector<long long> sccs;
void tarjan(long long num)
{
now++;
timestamp[num]=now;
low[num]=now;
vis[num]=true;
ss.push(num);
for(long long i=0;i<egs[num].size();i++)
{
long long nxt=egs[num][i];
if(timestamp[nxt]==0)
{
tarjan(nxt);
if(low[nxt]<low[num]){
low[num]=low[nxt];
}
}
else
{
if(vis[nxt] && timestamp[nxt]<low[num]){
low[num]=timestamp[nxt];
}
}
}
if(timestamp[num]==low[num])
{
while(ss.top()!=num)
{
long long temp=ss.top();
scc[temp]=num;//标记缩完点以后原来点的新编号
vis[temp]=false;//出栈了标记取消
ss.pop();
}
scc[num]=num;
sccs.push_back(num);//标记新图里面有哪些节点
ss.pop();
vis[num]=false;
}
}
void solve(long long ca)
{
long long n,m;
scanf("%lld%lld",&n,&m);
memset(timestamp,0,sizeof timestamp);
memset(low,0,sizeof low);
memset(vis,0,sizeof vis);
memset(mapper,0,sizeof mapper);
memset(scc,0,sizeof scc);
sccs.clear();
while (!ss.empty())
ss.pop();
for(int i=1;i<=n;i++)
{
egs[i].clear();
}
for(int i=0;i<m;i++)
{
int a,b;
cin>>a>>b;
egs[a].push_back(b);
}
for(long long i=1;i<=n;i++)
{
if(timestamp[i]==0)
{
tarjan(i);
}
}
for(int i=1;i<=n;i++)
{
for(int j=0;j<egs[i].size();j++)
{
int nxt=egs[i][j];
if(scc[i]!=scc[nxt])
{
mapper[scc[i]][scc[nxt]]=true;//临界矩阵表建新图(防止边重复)
}
}
}
for(int i=1;i<=n;i++)
for(auto it:mapper[i])
{
cout<<i<<" "<<it.first<<endl;
}
}
int main()
{
//freopen("test.txt","r",stdin);
long long t;
cin>>t;
for(long long i=1;i<=t;i++)
{
solve(i);
}
return 0;
}
求最近公共祖先
本算法是一个离线算法,在DFS过程中逐渐将子节点合并至父节点的并查集中,并在合并前找出子树中所有的查询答案。
代码
改修版POJ 1986 AC确认,该题只需要求两个子节点到公共祖先的距离,该题卡IO卡死,此处为通用版
#include <iostream>
#include <cstring>
#include <vector>
#include <cstdio>
using namespace std;
int n,m;
int uset[40005];
int ranker[40005];
struct edge
{
int to;
int dis;
};
vector<edge> eg1[40005];//存图
vector<edge> eg2[40005];//存查询
int dist[40005];
bool vis[40005];
int ans[40005];
int finder(int x)//找爹,并且缩短找爹的路径
{
if(x==uset[x])
{
return x;
}
else
{
uset[x]=finder(uset[x]);
return uset[x];
}
}
void usetter(int x,int y)//合并并查集
{
int fax=finder(x);
int fay=finder(y);
if(fax==fay)
{
return ;
}
if(ranker[fax]<ranker[fay])
{
uset[fay]=fax;
}
else if(ranker[fax]>ranker[fay])
{
uset[fax]=fay;
} else
{
uset[fay]=fax;
ranker[fay]++;
}
}
void tarjan(int num,int fa)
{
for(register int i=0;i<eg1[num].size();i++)
{
int next=eg1[num][i].to;
if(next!=fa)
{
tarjan(next,num);//遍历子节点
usetter(num,next);//把子节点和自己合并
}
}
for(register int i=0;i<eg2[num].size();i++)
{
int next=eg2[num][i].to;
if(vis[next])//另一个节点已经被遍历过了,那么它的并查集祖先一定是现在的最近公共祖先。
{
int lca=finder(next);
ans[eg2[num][i].dis]=lca;//记录答案以按顺序输出
}
}
vis[num]=true;//本节点已经被访问完毕,退出后即可和爸爸合并并查集,把公共祖先向上推一层
}
int main() {
cin>>n>>m;
memset(ranker,0,sizeof ranker);
memset(vis,0,sizeof vis);
memset(ans,0,sizeof ans);
memset(dist,0,sizeof dist);
for(register int i=1;i<=n;i++)
{
uset[i]=i;
eg1[i].clear();
eg2[i].clear();
}
for(register int i=0;i<m;i++)
{
int f1,f2;
cin>>f1>>f2;
edge temp;
temp.to=f1;
eg1[f2].push_back(temp);
temp.to=f2;
eg1[f1].push_back(temp);
}
int k;
cin>>k;
for(register int i=0;i<k;i++)//给查询也建一张图,方便DFS时找LCA
{
int fa,fb;
cin>>fa>>fb;
edge temp;
temp.to=fb;
temp.dis=i;//记录查询顺序
eg2[fa].push_back(temp);
temp.to=fa;
eg2[fb].push_back(temp);
}
tarjan(1,0);
for(register int i=0;i<k;i++)
{
cout<<ans[i]<<endl;
}
return 0;
}
参考了以下几个博主,表示感谢:
https://www.jianshu.com/p/d50ae711f946
https://www.cnblogs.com/c1299401227/p/5402747.html
https://www.byvoid.com/zhs/blog/scc-tarjanhttps://blog.csdn.net/u013480600/article/details/31743375