Tarjan
摘要图片来源:https://www.cnblogs.com/yanyiming10243247/p/9294160.html
Tarjan这东西,最基础的用法就是求强连通分量和环,然后还可以求割点、割边,以及可以缩点。看题目:
- 求强联通分量模板:P2863 [USACO06JAN]牛的舞会The Cow Prom
- 缩点模板:P3387 【模板】缩点
- 割点模板:P3388 【模板】割点(割顶)
- 割边模板:P1656 炸铁路
求强连通分量
首先,你需要知道:强连通、强连通图、强连通分量、dfs序、dfs树
那么,这都是些啥?在一个有向图中,点\([u,v]\)可以互相到达,那么我们就称\([u,v]\)是强连通。而强连通图,就是对于任意两个\([u,v]\)都能互相到达。而强连通分量,就是在一个子图中,满足对于任意\([u,v]\)都是强连通。
而dfs序,则是在dfs的过程中,第一次被遍历到的时间戳。dfs树就是在dfs过程中所形成的树,显然,从\(u\)dfs到\(v\),那\(u\)就是\(v\)的父节点,\(v\)就是\(u\)的子节点。不过这棵树还可能从一个结点回到自己的祖先。
那接下来就讲如何用Tarjan求强连通分量吧。首先,我们从任意一个点开始dfs,然后我们需要两个数组:\(low\)和\(dfn\)。\(low_i\)表示点\(i\)所在的强连通分量的根(我们把dfs树中的一个强连通分量看成一棵子树,那这个强连通分量的根就是在这个强连通分量中第一个被遍历到的点)。而\(dfn_i\)则表示点\(i\)的dfs序。接着,我们每遍历到一个点\(u\),就记录她的\(dfn\),然后让\(low_u\)的初值等于\(dfn_u\),再把她放进栈里。接着遍历她所连接的点\(v\),\(v\)有两种情况,一种情况是不在栈中,也就是没被遍历过,也就是儿子结点,那么就再做dfs(v),然后更新\(low_u = min(low_u,low_v)\)。第二种是在栈中,也就是是她的祖先,那么\(low_u = min(low_u,dfn_v)\)。最后,如果\(low_u\)还是等于\(dfn_u\),那就说明\(u\)是这个强连通分量的根,那么就让\(u\)和\(u\)上面的点全部出栈。你以为这样就好了吗?不是的,由于这个图不一定连通,所以最好循环\(1~n\),如果\(dfn\)没有更新,那么就是没走过,那就要做Tarjan。
接下来来看上面给的模板,明显是要求点的个数大于1的强连通分量的个数。(注意,只有1个点也是强连通分量)那我们只要在出栈是记录数量就可以了,因为在\(u\)上面的点都是\(u\)所在的强连通分量中的。
code:
#include<cstdio>
#include<stack>
using namespace std;
int n,m,ans;
int index,low[10005],dfn[10005],vis[10005];
//index用来记录当前搜了几个点,也就是当前dfn应该取几,而vis[i]表示以i为根的强连通分量有几个点
stack<int>s;
bool f[10005];//用来判断是否在栈里
struct graph
{
int tot,hd[10005];
int nxt[50005],to[50005];
void add(int x,int y)
{
tot++;
nxt[tot]=hd[x];
hd[x]=tot;
to[tot]=y;
return ;
}
}g;//链式前向星
void Tarjan(int x)
{
dfn[x]=low[x]=++index;
s.push(x);
f[x]=true;
//初始化
for(int i=g.hd[x];i;i=g.nxt[i])//遍历所有连通的点
if(!dfn[g.to[i]])//子节点
{
Tarjan(g.to[i]);//继续Tarjan
low[x]=min(low[x],low[g.to[i]]);//更新答案
}
else if(f[g.to[i]])//祖先
low[x]=min(low[x],dfn[g.to[i]]);//更新答案
if(dfn[x]==low[x])//如果是根
{
vis[x]=1;//更新vis
while(s.top()!=x)
{
f[s.top()]=false;
s.pop();
vis[x]++;//更新vis
}//出栈
f[x]=false;
s.pop();
//出栈
}
return ;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
g.add(u,v);
}
for(int i=1;i<=n;i++)
{
if(!dfn[i]) Tarjan(i);
if(vis[i]>1) ans++;
}
printf("%d",ans);
return 0;
}
缩点
什么是缩点呢?缩点,就是把一堆点缩成一个点。当然不能随便缩。如果有一堆点,她们能互相到达,也就是说这是一个强连通分量,那么把她们缩成一个点,是不是对题目本身没有影响?但缩完点之后他就变成了一个DAG,而且点和边的数量也大大减少了。这就是缩点。那么明白什么是缩点之后,那就很好写了,我们记录\(scc_i\),表示i所在的强连通分量的编号。然后对每个点遍历,对于点\(i\),把自己的权值赋给\(scc_i\),然后枚举所有出边,如果终点不在同一个强连通分量中,那么把这条边记录下来。注意,不能在原图上做,也就是我们要新建一个图。接下来,我们就可以根据题目要求做了。
接下来看模板,那这个就是缩完点之后,做一遍拓扑排序,然后求最大值。
code:
#include<cstdio>
#include<stack>
#include<queue>
using namespace std;
int mx,n,m,vv[10005];
int index,low[10005],dfn[10005];
stack<int>s;
int f[10005];
int scc_cnt,scc[10005];
queue<int>q;
int in[10005],vis[10005],dp[10005];
int max(int x,int y){return x>y?x:y;}
struct graph
{
int tot,hd[10005];
int nxt[100005],to[100005];
void add(int x,int y)
{
tot++;
nxt[tot]=hd[x];
hd[x]=tot;
to[tot]=y;
}
}g,sg;
void Tarjan(int x)//求强连通分量
{
dfn[x]=low[x]=++index;
s.push(x);
f[x]=true;
for(int i=g.hd[x];i;i=g.nxt[i])
if(!dfn[g.to[i]])
{
Tarjan(g.to[i]);
low[x]=min(low[x],low[g.to[i]]);
}
else if(f[g.to[i]])
low[x]=min(low[x],dfn[g.to[i]]);
if(dfn[x]==low[x])//出栈并记录scc
{
scc[x]=++scc_cnt;
while(s.top()!=x)
{
scc[s.top()]=scc_cnt;
f[s.top()]=false;
s.pop();
}
f[x]=false;
s.pop();
}
return ;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&vv[i]);
for(int i=1;i<=m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
g.add(u,v);
}
for(int i=1;i<=n;i++)
if(!dfn[i]) Tarjan(i);
for(int i=1;i<=n;i++)
{
vis[scc[i]]+=vv[i];
for(int j=g.hd[i];j;j=g.nxt[j])
{
int u=scc[i],v=scc[g.to[j]];
if(v!=u)
{
sg.add(u,v);
in[v]++;
}
}
}//建新图
for(int i=1;i<=scc_cnt;i++)
{
dp[i]=vis[i];
if(!in[i]) q.push(i);
}
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=sg.hd[x];i;i=sg.nxt[i])
{
int y=sg.to[i];
dp[y]=max(dp[y],dp[x]+vis[y]);
if(!--in[y]) q.push(y);
}
}
//拓扑排序
for(int i=1;i<=scc_cnt;i++)
mx=max(mx,dp[i]);//求得答案
printf("%d",mx);
return 0;
}
割点
割点,又叫割顶,这个东西的定义就是,在一个无向图中,如果有一个点\(u\),把她和连接她的边去掉后,连通数增加了,那么这个点\(u\)就是割点。
上面讲到的连通数,你可以理解为在无向图中的强连通分量的个数。
那这个割点怎么求呢?很简单,还是做Tarjan,然后,若一个点\(u\)的子节点中存在一个\(v\),使得\(low_v \ge dfn_u\),那么\(u\)就是割点。原因很简单,因为这样就表示有至少一棵子树不能到达她上面。但这只对不是根结点的点有效。那么根节点怎么判断呢?那就更简单了,只要有两棵及以上的子树,那她就一定是割点,原因自己想。
最后再提醒一下,并不是一个点连接的所有点都是她的子节点。
code:
#include<cstdio>
#include<stack>
#include<queue>
#include<vector>
using namespace std;
int n,m,ans,visit[20005];
int index,low[20005],dfn[20005];
stack<int>s;
bool f[20005];
vector<int>son[20005];
struct graph
{
int tot,hd[20005];
int nxt[500005],to[500005];
void add(int x,int y)
{
tot++;
nxt[tot]=hd[x];
hd[x]=tot;
to[tot]=y;
return ;
}
}g;
void Tarjan(int x)//做Tarjan
{
dfn[x]=low[x]=++index;
s.push(x);
f[x]=true;
for(int i=g.hd[x];i;i=g.nxt[i])
if(!dfn[g.to[i]])
{
Tarjan(g.to[i]);
low[x]=min(low[x],low[g.to[i]]);
son[x].push_back(g.to[i]);//记录子节点
}
else if(f[g.to[i]])
low[x]=min(low[x],dfn[g.to[i]]);
if(dfn[x]==low[x])
{
while(s.top()!=x)
{
f[s.top()]=false;
s.pop();
}
f[x]=false;
s.pop();
}
return ;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
g.add(u,v);
g.add(v,u);
}
for(int i=1;i<=n;i++)
if(!dfn[i]) Tarjan(i);
for(int i=1;i<=n;i++)
{
if(dfn[i]==low[i])//特判根结点
{
if(son[i].size()>1) visit[++ans]=i;
continue;
}
for(int j=0;j<son[i].size();j++)
if(low[son[i][j]]>=dfn[i])//满足条件
{
visit[++ans]=i;
break;
}
}
printf("%d\n",ans);
for(int i=1;i<=ans;i++) printf("%d ",visit[i]);
//输出答案
return 0;
}
割边
割边又叫割桥,割边的定义和割点相似,就是如果去掉一条边使得连通数增加,那么这条边就是割边。
割边的求法也和割点相似,就是对于一条边\([u,v]\),若\(low_v > dfn_u\),那么这条边就是割边。这里不取等于的原因是,若\(v\)可以通过另一条边到达\(u\),那么这条边自然不是割边了。
割边不用判断根节点(毕竟找的是边不是点),但需要特判通往父节点的那条边(原因显然)。
接下来看模板,这题就是在求完割边之后要再排一遍序,确保输出的是有序的。
code:
#include<cstdio>
#include<stack>
#include<queue>
#include<vector>
#include<algorithm>
using namespace std;
int n,m,ans;
int index,low[20005],dfn[20005];
stack<int>s;
bool f[20005];
vector<int>son[20005];
struct srt
{
int u,v;
}a[20005];
bool cmp(srt x,srt y)
{
return x.u<y.u||(x.u==y.u&&x.v<y.v);
}
struct graph
{
int tot,hd[20005];
int nxt[500005],to[500005];
void add(int x,int y)
{
tot++;
nxt[tot]=hd[x];
hd[x]=tot;
to[tot]=y;
return ;
}
}g;
void Tarjan(int x,int dad)
{
dfn[x]=low[x]=++index;
s.push(x);
f[x]=true;
for(int i=g.hd[x];i;i=g.nxt[i])
if(!dfn[g.to[i]])
{
Tarjan(g.to[i],x);
low[x]=min(low[x],low[g.to[i]]);
son[x].push_back(g.to[i]);
}
else if(f[g.to[i]]&&g.to[i]!=dad)//特判父节点
low[x]=min(low[x],dfn[g.to[i]]);
if(dfn[x]==low[x])
{
while(s.top()!=x)
{
f[s.top()]=false;
s.pop();
}
f[x]=false;
s.pop();
}
return ;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int u,v;
scanf("%d%d",&u,&v);
g.add(u,v);
g.add(v,u);
}
for(int i=1;i<=n;i++)
if(!dfn[i]) Tarjan(i,0);
for(int i=1;i<=n;i++)
{
for(int j=0;j<son[i].size();j++)//上面两重循环是枚举每条边
if(low[son[i][j]]>dfn[i])//符合条件
{
ans++;
a[ans].u=i;
a[ans].v=son[i][j];
}
}
sort(a+1,a+ans+1,cmp);//注意要按顺序输出哦
for(int i=1;i<=ans;i++)
printf("%d %d\n",a[i].u,a[i].v);
return 0;
}