//悲观者永远正确,乐观者永远前行。|

ccrui

园龄:2年2个月粉丝:2关注:4

2023-08-25 10:56阅读: 22评论: 0推荐: 0

Tarjan学习笔记


Tarjan

Tarjan算法是图论中非常常用的算法之一,能解决强连通分量,双连通分量,割点和桥,求最近公共祖先(LCA)等问题。

Tarjan 算法是基于深度优先搜索的算法,用于求解图的连通性问题。

割点

如果从图中删除节点 x 以及所有与 x 关联的边之后,图将被分成两个或两个以上的不相连的子图,那么称 x 为图的割点。
image

如3、5就是图的割点

桥/割边

如果从图中删除边 e 之后,图将分裂成两个不相连的子图,那么称 e 为图的桥/割边。
image

如图中边 (3,5) 就是图的割边

实现

几个定义

强连通分量 :
对于一个分量,若任意两个点相通,则称为强连通分量。

树边 :
对于一个图的dfs树,它的树边便是此图的树边。

返祖边 :
对于一个图的dfs树,可以使得儿子节点返回到它的祖先的边为返祖边。

横插边 :
对于一个图的dfs树,可以使得一个节点到达另一个节点且它们互不是祖先的边为横插边。

连通

连通:无向图中,从任意点 i 可到达任一点 j
强连通:有向图中,从任意点 i 可到达任一点 j
弱连通:把有向图看作无向图时,从任意点i可到达任一点 j
image

强连通分量

整个图并不是强连通的,但是在某些局部区域符合强连通的要求,如下图,整张图不算是强连通,但局部还是能满足强连通条件的。
image

时间戳

时间戳是用来标记图中每个节点在进行dfs时被访问的顺序,可以理解成一个由小到大的序号(类似于dfs序)。

搜索树

在无向图中,以某一个节点 x 出发进行dfs,每一个节点只访问一次,所有被访问过的节点和边构成一棵树,称之为“无向连通图的搜索树”。

追溯值

追溯值用来表示从当前节点 x 能够访问到的所有节点中,时间戳最小的值。
能够访问到的节点其需要满足下面的条件之一:

  • x 为根的搜索树的所有节点。
  • 通过一条非搜索树上的边,能够到达搜索树的所有节点。

代码

dfn:第 i 个节点的时间戳。

low:第 i 个节点最多经过一条返祖边所能到达的最小时间戳。

s:一个栈,用来储存当前还未确定但已经扩展过的点。

b:第 i 个节点是否遍历过。

ans:答案计数。

low 值与 dfn 值判断:

  1. 如果一个节点的 low 值小于 dfn 值,那么就说明它或者它的子孙节点有边连到自己上方的节点。

  2. 如果一个节点的 low 值等于 dfn 值,则说明其下方的节点不能走到其上方节点,那么该节点就是一个强连通分量在搜索树中的根。

  3. 但是 u 的子孙节点就未必和 u 处于同一个强连通分量,用栈存储即可。

void tarjan(int 当前点){
这个点的low=dfn=时间戳;
...
for(这个点连接的所有边){
if(目标点没有被访问过){
tarjan(目标点);
更新当前点的low;
...
}else if(目标点被访问过){
更新当前点的low;
...
}
}
...
}
int dfn[100010],low[100010];
int n,m,num=0,ans=0;
vector<int>v[100010];
stack<int>s;
void tarjan(int u){
dfn[u]=low[u]=++num;
...
for(int i=0;i<v[u].size();i++) {
int nn=v[u][i];
if(!dfn[nn]){
tarjan(nn);
low[u]=min(low[u],low[nn]);
...
}else if(...){
low[u]=min(low[u],dfn[nn]);
...
}
}
...
}

调用:

for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i),sum=max(sum,ans);//最大强连通分量sum

LCA code

【模板】最近公共祖先(LCA)

O(n+m)

并查集维护祖先。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,s;
struct ask{
int a,b;
};
vector<ask>quer[1000100];
vector<int>v[1000100];
int fa[1000001],k[1000001],d[10000001],ans[10000001];
int find(int x){
if(fa[x]==x)return x;
else return fa[x]=find(fa[x]);
}
void tarjan(int x){
k[x]=1;
for(auto i:v[x]){
if(k[i])continue;
d[i]=d[x]+1;
tarjan(i);
fa[i]=x;
}
for(int i=0;i<quer[x].size();i++){
int y=quer[x][i].a,id=quer[x][i].b;
if(k[y]==2){
int lca=find(y);
ans[id]=lca;
}
}
k[x]=2;
}
signed main(){
cin>>n>>m>>s;
for(int i=0;i<=n;i++)fa[i]=i;
for(int i=1;i<n;i++){
int uu,vv;
cin>>uu>>vv;
v[uu].push_back(vv);
v[vv].push_back(uu);
}
for(int i=1;i<=m;i++){
int uu,vv;
cin>>uu>>vv;
if(uu==vv)ans[i]=uu;
quer[uu].push_back(ask{vv,i});
quer[vv].push_back(ask{uu,i});
}
tarjan(s);
for(int i=1;i<=m;i++){
cout<<ans[i]<<endl;
}
return 0;
}

强连通分量code

[USACO06JAN] The Cow Prom S

用一个栈维护强连通部分+染色

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,num,cnt,ans;
stack<int>s;
vector<int>v[1000100];
int dfn[100010],low[100010],b[100010];
int color[100010],cols=0,cl[100010];
void paint(){
color[s.top()]=cols;
cl[cols]++;
b[s.top()]=0;
}
void tarjan(int u){
num++;
dfn[u]=low[u]=num;
s.push(u);
b[u]=1;
for(int i=0;i<v[u].size();i++) {
int nn=v[u][i];
if(!dfn[nn]){
tarjan(nn);
low[u]=min(low[u],low[nn]);
}else if(b[nn]){
low[u]=min(low[u],dfn[nn]);
}
}
if(low[u]==dfn[u]){
cols++;
while(!s.empty()&&s.top()!=u){
paint();
s.pop();
}
paint();
s.pop();
}
}
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int uu,vv;
cin>>uu>>vv;
v[uu].push_back(vv);
}
for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i);
for(int i=1;i<=cols;i++){
if(cl[i]>1)ans++;
//cout<<cl[i]<<" ";
}
cout<<ans<<endl;
return 0;
}

割点/割边(桥)

判定

割点:如果一个点 u 为割点,那么有两种情况:

  1. u 为树根,且有超过一个子树。

  2. u 不为树根,且满足存在 (u,v) 为树枝边,使得 dfn(u)low(v)

桥:如果一条无向边 (u,v) 是桥,当且仅当 (u,v) 为树枝边,且满足 dfn[u]<low[v] (前提是这条边不存在重边)。

求割点code

P3388 【模板】割点(割顶)

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,root,num,cnt,ans;
vector<int>v[1000100];
int dfn[100010],low[100010],st[100010],b[100010];
void tarjan(int x){
int son=0;
dfn[x]=low[x]=++num;
s.push(x);
b[x]=1;
for(int i=0;i<v[x].size();i++) {
int nn=v[x][i];
if(!dfn[nn]){
tarjan(nn);
son++;
low[x]=min(low[x],low[nn]);
if(low[nn]>=dfn[x]&&x!=root&&!st[x]){
cnt++;st[x]=1;
}
}else if(b[nn]){
low[x]=min(low[x],dfn[nn]);
}
}
if(son>=2&&x==root&&!st[x]){
cnt++;st[x]=1;
}
}
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int uu,vv;
cin>>uu>>vv;
v[uu].push_back(vv);
v[vv].push_back(uu);
}
for(int i=1;i<=n;i++)if(!dfn[i])root=i,tarjan(i);
cout<<cnt<<endl;
for(int i=1;i<=n;i++){
if(st[i])cout<<i<<" ";
}
return 0;
}

求割边code

P1656 炸铁路

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,num,cnt,ans;
stack<int>s;
vector<int>v[1000100];
int dfn[100010],low[100010],b[100010];
struct edge{
int l,r;
bool operator <(const edge bb)const{
if(l==bb.l)return r<bb.r;
return l<bb.l;
}
}e[100010];
int es=0;
void tarjan(int x,int la){
dfn[x]=low[x]=++num;
b[x]=1;
for(int i=0;i<v[x].size();i++) {
int nn=v[x][i];
if(!dfn[nn]){
tarjan(nn,x);
low[x]=min(low[x],low[nn]);
if(low[nn]>dfn[x]){
if(x>nn)e[es++]=edge{nn,x};
else e[es++]=edge{x,nn};
}
}else if(nn!=la){
low[x]=min(low[x],dfn[nn]);
}
}
}
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int uu,vv;
cin>>uu>>vv;
v[uu].push_back(vv);
v[vv].push_back(uu);
}
for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i,0);
sort(e,e+es);
for(int i=0;i<es;i++)cout<<e[i].l<<" "<<e[i].r<<endl;
return 0;
}

缩点code

无非就是把染色那加了缩点(即删除节点 y,增加 u 权值的操作)

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,num,cnt,ans;
stack<int>s;
vector<int>v[1000100];
int dfn[100010],low[100010],b[100010];
int col[100010],p[100010];
void tarjan(int u){
dfn[u]=low[u]=++num;
s.push(u);
b[u]=1;
for(int i=0;i<v[u].size();i++) {
int nn=v[u][i];
if(!dfn[nn]){
tarjan(nn);
low[u]=min(low[u],low[nn]);
}else if(b[nn]){
low[u]=min(low[u],dfn[nn]);
}
}
if(low[u]==dfn[u]){
while(!s.empty()&&s.top()!=u){
int y=s.top();
col[y]=u;
b[y]=0;
if(u==y)break;
p[u]+=p[y];
s.pop();
}
col[u]=u;
b[u]=0;
s.pop();
}
}
int ru[100010];
vector<int>nv[100010];
int dis[100010],vis[100010];
int getans(){
queue<int>q;
int tot=0;
for(int i=1;i<=n;i++)
if(col[i]==i&&!ru[i]){
q.push(i);
dis[i]=p[i];
}
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<nv[u].size();i++){
int y=nv[u][i];
dis[y]=max(dis[y],dis[u]+p[y]);
ru[y]--;
if(ru[y]==0)q.push(y);
}
}
int ans=0;
for(int i=1;i<=n;i++)ans=max(ans,dis[i]);
return ans;
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>p[i];
for(int i=1;i<=m;i++){
int uu,vv;
cin>>uu>>vv;
v[uu].push_back(vv);
}
for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i);
for(int i=1;i<=n;i++){
for(auto k:v[i]){
int x=col[i],y=col[k];
if(x!=y){
nv[x].push_back(y);
ru[y]++;
}
}
}
cout<<getans()<<endl;
return 0;
}

双连通分量

点双连通分量code

P8435 【模板】点双连通分量

点双连通:在一个无向图中,若任意两点间至少存在两条“点不重复”的路径。
点双连通分量:一个子图满足点双连通且在图 G 中是极大联通子图

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,num,cnt,ans;
vector<int>anss[5000101];
stack<int>s;
vector<int>v[5000100];
int dfn[5000010],low[5000010],b[5000010];
int ru[5000010];
void tarjan(int x,int fa){
int son=0;
dfn[x]=low[x]=++num;
s.push(x);
for(int i=0;i<v[x].size();i++) {
int nn=v[x][i];
if(!dfn[nn]){
tarjan(nn,x);
son++;
low[x]=min(low[x],low[nn]);
if(low[nn]>=dfn[x]){
cnt++;
int p;
do{
p=s.top();
s.pop();
anss[cnt].push_back(p);
}while(p!=nn);
anss[cnt].push_back(x);
}
}else if(nn!=fa){
low[x]=min(low[x],dfn[nn]);
}
}
if(!son&&!fa){
anss[++cnt].push_back(x);
}
}
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int uu,vv;
cin>>uu>>vv;
if(uu==vv)continue;
ru[vv]++,ru[uu]++;
v[uu].push_back(vv);
v[vv].push_back(uu);
}
for(int i=1;i<=n;i++){
if(!dfn[i]){
while(!s.empty())s.pop();
tarjan(i,0);
}
}
cout<<cnt<<endl;
for(int i=1;i<=cnt;i++){
cout<<anss[i].size()<<" ";
for(auto j:anss[i])cout<<j<<" ";
cout<<endl;
}
return 0;
}

边双连通分量code

P8436 【模板】边双连通分量

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m,num,ans;
vector<int>v[1000100];
int dfn[1000010],low[1000010],b[1000010];
int vs=0,cnt=1,head[2000005];
bool vis[2000005];
struct edge{
int to,next,q;
}e[5000010];
void add(int u,int v){
e[++cnt].to=v;
e[cnt].q=0;
e[cnt].next=head[u];
head[u]=cnt;
}
void tarjan(int x,int la){
dfn[x]=low[x]=++num;
for(int i=head[x];i;i=e[i].next){
int nn=e[i].to;
if(!dfn[nn]){
tarjan(nn,x);
low[x]=min(low[x],low[nn]);
if(low[nn]>dfn[x]){
e[i].q=e[i^1].q=1;
}
}else if(nn!=la){
low[x]=min(low[x],dfn[nn]);
}
}
}
void dfs(int x){
v[vs].push_back(x);
b[x]=1;
for(int i=head[x];i;i=e[i].next){
if(!b[e[i].to]&&!e[i].q)dfs(e[i].to);
}
}
signed main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int uu,vv;
cin>>uu>>vv;
add(uu,vv);
add(vv,uu);
}
for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i,0);
for(int i=1;i<=n;i++){
if(!b[i])vs++,dfs(i);
}
cout<<vs<<endl;
for(int i=1;i<=vs;i++){
cout<<v[i].size()<<" ";
for(auto j:v[i])cout<<j<<" ";
cout<<endl;
}
return 0;
}

本文作者:ccrui

本文链接:https://www.cnblogs.com/ccr-note/p/Tarjan.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   ccrui  阅读(22)  评论(0编辑  收藏  举报
评论
收藏
关注
推荐
深色
回顶
收起
点击右上角即可分享
微信分享提示