Tarjan用法总结【模板预备】

Tarjan

可缩点、DAG有向无环图、强连通分量及其出入度,割点、割边、点双联通分量、边双连通分量,LCA。

一、基础算法及理解

dfn[x]:访问到该节点的时间戳。

low[x]:x和x子树中的所有节点,通过一条边(除去树边),可到达的节点最小时间戳。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+6;
vector<int>G[maxn];
int tot,tp,cnt;
int dfn[maxn],low[maxn],st[maxn],in[maxn],col[maxn];
int v[maxn],val[maxn];
void tarjan(int x){
	dfn[x]=low[x]=++tot;
	st[++tp]=x;
	in[x]=1;
	for(int i=0;i<G[x].size();i++){
		int v=G[x][i];
		if(!dfn[v]){
			tarjan(v);
			low[x]=min(low[x],low[v]);
		}
		else if(in[v])
		low[x]=min(low[x],dfn[v]);
	}
	if(dfn[x]==low[x]){
		cnt++;
		col[x]=cnt;
		val[cnt]=v[x];
		in[x]=0;
		while(st[tp]!=x){
			col[st[tp]]=cnt;
			val[cnt]+=v[st[tp]];
			in[st[tp]]=0;
			tp--;
		}
		tp--;//这里小心!
	}
}

在栈中的节点,他自己子节点的遍历任务没有完成,也就是说子树中还有节点的遍历没完成,假设此时时间停止,那么此时正在进行的遍历的节点x,是栈中所有节点的子树的一份子(假设时间停止在对x的子节点遍历之前,可以理解成刚进入tarjan(x)函数还未进行其他操作)。所以当前的x和栈中的节点是祖先关系,即栈中元素可达x。

设tarjan遍历没有遇到阻碍(即dfn==0)时经过的边为树边,由所谓树边连接的称父子节点。

根据low[x]定义,是x及其子树跨过某条边可达的节点的dfn的最小dfn值,同时二者彼此可达

  1. 那么对于同一棵子树,直接取每个子节点的low值更新即可。l

  2. 对于非子树的节点v,如果还在栈中,那么v和x其实早就有“先后”顺序了,而此时的“当前正在进行的遍历的点x”,又有新边(不可能称为树边了,“祖先边”)连接到v,由此二者彼此可达。用dfn更新完美地符合定义。

二、相关概念

  1. 强连通分量:分量内部的节点彼此可达。

  2. 点双联通分量:分量内部没有割点

  3. 边双连通分量:分量内部没有割边(桥)

三、易错点

注意有无孤立点、重边,自环;注意有向图和无向图的算法区别。

割点可忽视重边,割边不可忽视重边

四、tarjan求有向图

1. 求强连通分量(缩点)

https://www.luogu.com.cn/problem/T103440

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+6;
vector<int>G[maxn];
int tot,tp,cnt;
int dfn[maxn],low[maxn],st[maxn],in[maxn],col[maxn];
int v[maxn],val[maxn];
void tarjan(int x){
	dfn[x]=low[x]=++tot;
	st[++tp]=x;
	in[x]=1;
	for(int i=0;i<G[x].size();i++){
		int v=G[x][i];
		if(!dfn[v]){
			tarjan(v);
			low[x]=min(low[x],low[v]);
		}
		else if(in[v])
		low[x]=min(low[x],dfn[v]);
	}
	if(dfn[x]==low[x]){
		cnt++;
		col[x]=cnt;
		val[cnt]=v[x];
		in[x]=0;
		while(st[tp]!=x){
			col[st[tp]]=cnt;
			val[cnt]+=v[st[tp]];
			in[st[tp]]=0;
			tp--;
		}
		tp--;//这里小心!
	}
}
int vis[maxn];
int main(){
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>v[i];
	}
	int u,v;
	for(int i=1;i<=m;i++){
		cin>>u>>v;
		G[u].push_back(v);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i])tarjan(i);
	}
	for(int i=1;i<=n;i++){
		if(!vis[col[i]]){
			cout<<val[col[i]]<<"\n";
			vis[col[i]]=1;
		}
	}
} 

2. 求出度入度

https://ac.nowcoder.com/acm/contest/76/E?&headNav=www

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+6;
vector<int>G[maxn];
int tot,tp,cnt;
int dfn[maxn],low[maxn],st[maxn],in[maxn],col[maxn];
int in_[maxn],out_[maxn];

void tarjan(int x){
	dfn[x]=low[x]=++tot;
	st[++tp]=x;
	in[x]=1;
	for(int i=0;i<G[x].size();i++){
		int v=G[x][i];
		if(!dfn[v]){
			tarjan(v);
			low[x]=min(low[x],low[v]);
		}
		else if(in[v])
		low[x]=min(low[x],dfn[v]);
	}
	if(dfn[x]==low[x]){
		cnt++;
		col[x]=cnt;
		in[x]=0;
		while(st[tp]!=x){
			col[st[tp]]=cnt;
			in[st[tp]]=0;
			tp--;
		}
		tp--;
	}
}
int vis[maxn],vis_[maxn];
int main(){
	int n,m;
	cin>>n>>m;
	int x;
	for(int i=1;i<=m;i++){
		cin>>x;
		vis[x]=1;
	}
	int u,v,num_;
	for(int i=1;i<=n;i++){
		cin>>num_;
		for(int j=0;j<num_;j++){
			cin>>v;
			G[i].push_back(v);
		}
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i])tarjan(i);
	}
	for(int i=1;i<=n;i++){
		if(vis[i])
		vis_[col[i]]=1;
	}
	for(int i=1;i<=n;i++){
		for(int j=0;j<G[i].size();j++){
			int v=G[i][j];
			if(col[i]!=col[v]){
				out_[col[i]]++;//连通块出度 
				in_[col[v]]++;//连通块入度 
			}
		}
	} 
	int cnt_=0;
	for(int i=1;i<=cnt;i++){
		if(in_[i]==0)cnt_++;
	}
	if(cnt_>m){
		printf("-1\n");
		return 0;
	}
	int ans=0;
	for(int i=1;i<=cnt;i++){
		if(in_[i]==0){
			if(vis_[i]==0){
				printf("-1\n");
				return 0;
			} 
			else{
				ans++;
			}
		}
	}
	cout<<ans<<"\n";
} 

3. 缩点+出入度 重构成DAG

https://www.luogu.com.cn/problem/P3387

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#include<map>
using namespace std;
#define pii pair<int,int>
const int maxn=1e6+6;
vector<int>G[maxn];
vector<int>g[maxn];
int v[maxn],val[maxn];
int dfn[maxn],low[maxn],col[maxn],st[maxn],in[maxn];
int out_[maxn],in_[maxn];
int tot,cnt,tp;
void tarjan(int x){
	dfn[x]=low[x]=++tot;
	in[x]=1;
	st[++tp]=x;
	for(int i=0;i<G[x].size();i++){
		int v=G[x][i];
		if(!dfn[v]){
			tarjan(v);
			low[x]=min(low[x],low[v]);
		}else if(in[v])low[x]=min(low[x],dfn[v]);
	}
	if(dfn[x]==low[x]){
		cnt++;
		col[x]=cnt;
		in[x]=0;
		val[cnt]=v[x];
		while(st[tp]!=x){
			col[st[tp]]=cnt;
			in[st[tp]]=0;
			val[cnt]+=v[st[tp]];
			tp--;
		}
		tp--;
	}
}
void rebuild(int x){
	for(int i=0;i<G[x].size();i++){
		int v=G[x][i];
		if(col[x]!=col[v]){
			g[col[x]].push_back(col[v]);
		//	out_[col[x]]++;
			in_[col[v]]++;
		}
	} 
}
int sum[maxn];
int dfs(int x){
	if(sum[x]!=0)return sum[x];
	sum[x]=val[x];
	int tmp=0;
	for(int i=0;i<g[x].size();i++){
		int v=g[x][i];
		tmp=max(tmp,dfs(v));
	}
	sum[x]+=tmp;
	return sum[x];
}
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)scanf("%d",&v[i]);
	for(int i=1;i<=m;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		G[u].push_back(v);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i])tarjan(i);
	}
	for(int i=1;i<=n;i++){
		rebuild(i);
	}
	for(int i=1;i<=cnt;i++){
		if(in_[i]==0){
			dfs(i);
		}
	}
	int ans=0;
	for(int i=1;i<=cnt;i++){
		ans=max(ans,sum[i]);
	}
	cout<<ans<<"\n";
} 

五、tarjan求无向图

1. 求割点

https://www.luogu.com.cn/problem/P3388

#include<bits/stdc++.h>
using namespace std;
const int maxn=3e5+6;
vector<int>G[maxn];
int dfn[maxn],low[maxn],snum[maxn];
int deep;
void tarjan(int u,int fa){
    dfn[u]=++deep;
    low[u]=deep;
    int sz=G[u].size();
    for(int i=0;i<sz;i++){
        int v=G[u][i];
        if(!dfn[v]){
            tarjan(v,u);
            low[u]=min(low[u],low[v]);
            if(low[v]>=dfn[u])snum[u]++; //u未必就是割点(u是子树根时)
        }
        else if(v!=fa)low[u]=min(low[u],dfn[v]);//和有向图的区别
    }
}
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=0;i<m;i++){
		int aa,bb;
		scanf("%d%d",&aa,&bb);
		G[aa].push_back(bb);
		G[bb].push_back(aa);
	}
	int ans=0;
	for(int i=1;i<=n;i++){
		if(!dfn[i])tarjan(i,i),ans++,snum[i]--;//根节点需要snum[i]-- 
	}
	//snum[i]:去掉i后,增加的联通块数。
    //snum[i]>0时,i才为割点。 
}

2. 求割边

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=1e6+6;
vector<int>G[maxn];
int dfn[maxn],low[maxn],num[maxn];
int tot;
#define pii pair<int,int>
vector<pii>vc;
void tarjan(int x,int fa){
	dfn[x]=low[x]=++tot;
	for(int i=0;i<G[x].size();i++){
		int v=G[x][i];
		if(!dfn[v]){
			tarjan(v,x);
			low[x]=min(low[x],low[v]);
			if(low[v]>dfn[x]){
				vc.push_back(pii(x,v));//割边 
			}
		}
		else if(v!=fa)low[x]=min(low[x],dfn[v]); 
	}
}
int main(){
	int n,m;
	cin>>n>>m;
	int u,v;
	for(int i=1;i<=m;i++){
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	int ans=0; 
	for(int i=1;i<=n;i++){
		if(!dfn[i]){
			tarjan(i,0);
			ans++;
		}
	}
	cout<<vc.size()+ans<<"\n"; //求出整个图中,边双联通分量的数目
} 

3. 求出边双联通分量的数量

https://www.luogu.com.cn/problem/T103489

只需要将上面求割边代码中的main函数改为:

int main(){
	int n,m;
	cin>>n>>m;
	int u,v;
	for(int i=1;i<=m;i++){
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	int ans=0; 
	for(int i=1;i<=n;i++){
		if(!dfn[i]){
			tarjan(i,0);
			ans++;
		}
	}
	cout<<vc.size()+ans<<"\n"; //求出整个图中,边双联通分量的数目
} 

即可。

4. 求出每一个点双联通分量

https://www.luogu.com.cn/problem/T103492

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int maxn=1e6+6;
vector<int>G[maxn];
int dfn[maxn],low[maxn],in[maxn],st[maxn];
int tot,tp;
void tarjan(int x,int fa){
	dfn[x]=low[x]=++tot;
	st[++tp]=x;
	if(fa==0&&G[x].size()==0){//注意这里 孤立点的处理 
		cout<<x<<"\n";
		return;
	}
	for(int i=G[x].size()-1;i>=0;i--){
		int v=G[x][i];
		if(!dfn[v]){
			tarjan(v,x);
			low[x]=min(low[x],low[v]);
			if(low[v]>=dfn[x]){
				int z;
				do{
					z=st[tp--];
					cout<<z<<" ";
				}while(z!=v);
				cout<<x<<"\n";
            }    
		/*	这里不用以下的代码: 
			while(st[tp]!=x){
				cout<<st[tp]<<" ";
				tp--;
			}
			cout<<x<<"\n";
			原因是,v后面未必就是x。
			栈里面的数据是点第一次被访问就进去一个新的,
			但是出栈却是有更严格的条件:需要low[v]>=dfn[x]。
			进和出的条件不在同一水平。 
		*/ 	
		}
		else if(v!=fa)low[x]=min(low[x],dfn[v]); 
	}
}
int main(){
	int n,m;
	cin>>n>>m;
	int u,v;
	for(int i=1;i<=m;i++){
		cin>>u>>v;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]){
			tarjan(i,0);
		}
	}
} 

六、tarjan求树

1. 求LCA[离线求法]

https://www.luogu.com.cn/problem/P3379

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
#include<map>
using namespace std;
#define pii pair<int,int>
const int maxn=1e6+6;
vector<int>G[maxn];
vector<int>Q[maxn];
map<pii,int>mp;
int vis[maxn],fa[maxn];
int ffa(int x){
	if(fa[x]==x)return x;
	return fa[x]=ffa(fa[x]);
	//又是这里出锅 
	//1.return fa[x]=ffa(x);
	//2.return ffa(fa[x]);
	//以上两种皆会T 
}
void tarjan(int x){
	vis[x]=1;
	fa[x]=x;
	for(int i=0;i<G[x].size();i++){
		int v=G[x][i];
		if(vis[v])continue;
		tarjan(v);
		fa[v]=x;//这里感觉是精髓!
	}
	for(int i=0;i<Q[x].size();i++){
		int v=Q[x][i];
		if(vis[v]){
			mp[pii(x,v)]=mp[pii(v,x)]=ffa(v);
		}
	}
}
struct Query{
	int u,v;
}query[maxn];
int main(){
	int n,m,rt;
	scanf("%d%d%d",&n,&m,&rt);
	for(int i=1;i<n;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		G[u].push_back(v);
		G[v].push_back(u);
	}
	for(int i=1;i<=m;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		query[i].u=u;
		query[i].v=v;
		Q[u].push_back(v);
		Q[v].push_back(u);
	}
	tarjan(rt);
	for(int i=1;i<=m;i++){
        cout<<mp[pii(query[i].u,query[i].v)]<<"\n";
	}
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值