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值,同时二者彼此可达。
-
那么对于同一棵子树,直接取每个子节点的low值更新即可。l
-
对于非子树的节点v,如果还在栈中,那么v和x其实早就有“先后”顺序了,而此时的“当前正在进行的遍历的点x”,又有新边(不可能称为树边了,“祖先边”)连接到v,由此二者彼此可达。用dfn更新完美地符合定义。
二、相关概念
-
强连通分量:分量内部的节点彼此可达。
-
点双联通分量:分量内部没有割点
-
边双连通分量:分量内部没有割边(桥)
三、易错点
注意有无孤立点、重边,自环;注意有向图和无向图的算法区别。
割点可忽视重边,割边不可忽视重边
四、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";
}
}