tarjan学习笔记

发现\(tarjan\)快忘光了,来补个学习笔记

\(tarjan\)缩点

维护两个东西,\(dfn_u\)\(low_u\)

分别表示\(dfs\)序和该子树中所能回溯到的\(dfs\)序最小的点的\(dfs\)

发现当一个点的\(dfn\)\(low\)相同时,这个点一定在强连通分量中(因为可以回到它自己)

怎么获得同一强连通分量中的其他点呢?

开一个栈,记录还未进入强连通分量的点

发现强连通分量后,弹栈直到自己,能够保证都是其子树中的节点

使用 \(tarjan\)的时候一定是这样!!!

for(int i=1;i<=n;i++){
	if(!dfn[i]) tarjan(i);
}

千万不要只跑一次!!!

例题代码

#include<bits/stdc++.h>
#define usetime() (double)clock () / CLOCKS_PER_SEC * 1000.0
using namespace std;
typedef long long LL;
const int maxn=1e4+5;
const int maxm=1e5+5;
void read(int& x){
	char c;
	bool f=0;
	while((c=getchar())<48) f|=(c==45);
	x=c-48;
	while((c=getchar())>47) x=x*10+c-48;
	x=(f ? -x : x);
	return;
}
class mmap{
public:
	int head[maxn],nxt[maxm],e[maxm];
	int mp_cnt;
	void init_mp(){
		memset(head,-1,sizeof(head));
		mp_cnt=-1;
	}
	void add_edge(int u,int v){
		e[++mp_cnt]=v;
		nxt[mp_cnt]=head[u];
		head[u]=mp_cnt;
	}
}mp1,mp2;
int dfn[maxn],low[maxn],id[maxn];
bool ins[maxn];
int a[maxn];
int dp[maxn],b[maxn],in[maxn];
stack<int> st;
int n,m;
int tot=0;
int cnt=0;
void tarjan(int u){
	dfn[u]=low[u]=++tot;
	ins[u]=1;
	st.push(u);
	for(int i=mp1.head[u];~i;i=mp1.nxt[i]){
		int v=mp1.e[i];
		if(!dfn[v]){
			tarjan(v);
			low[u]=min(low[u],low[v]);
		}
		else if(ins[v]){
			low[u]=min(low[u],dfn[v]);
		}
	}
	if(dfn[u]==low[u]){
		++cnt;
		int now=-1;
		while(now!=u){
			now=st.top(),st.pop();
			id[now]=cnt,ins[now]=0;
			b[cnt]+=a[now];
		}
	}
}
int topo(){
	queue<int> q;
	int ans=0;
	for(int i=1;i<=cnt;i++){
		if(!in[i]){
			q.push(i);
			dp[i]=b[i];
		}
	}
	while(!q.empty()){
		int u=q.front();
		q.pop();
		ans=max(ans,dp[u]);
		for(int i=mp2.head[u];~i;i=mp2.nxt[i]){
			int v=mp2.e[i];
			dp[v]=max(dp[v],dp[u]+b[v]);
			--in[v];
			if(!in[v]) q.push(v);
		}
	}
	return ans;
}
int main(){
	read(n),read(m);
	for(int i=1;i<=n;i++){
		read(a[i]);
	}
	int u,v;
	mp1.init_mp();
	for(int i=1;i<=m;i++){
		read(u),read(v);
		mp1.add_edge(u,v);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]) tarjan(i);
	}
	mp2.init_mp();
	for(int i=1;i<=n;i++){
		for(int j=mp1.head[i];~j;j=mp1.nxt[j]){
			v=mp1.e[j];
			if(id[i]!=id[v]) mp2.add_edge(id[i],id[v]),++in[id[v]];
		}
	}
	printf("%d",topo());
	return 0;
}
//^o^

缩点可以把一张有向图转为一张有向无环图(\(DAG\)),和拓扑排序通常一起使用

然后是非常常见的割点与桥

\(tarjan\)都是同一个原理,如果是割边它所连接的子节点的 \(low\) 大于它连得父节点 \(dfn\)

也就是说出了这一条边,子节点没有其他路径可以回到父节点了,此时图就不连通了

板题

#include<bits/stdc++.h>
using namespace std;
const int maxn=155;
void read(int& x){
	char c;
	while((c=getchar())<48);
	x=c-48;
	while((c=getchar())>47) x=x*10+c-48;
	return;
}
struct edge{
	int u,v;
};
bool cmp(edge a,edge b){
	return a.u==b.u ? a.v<b.v : a.u<b.u;
}
int mini(int x,int y){
	return x<y ? x : y;
}
int n,m;
int dfn[maxn],low[maxn];
vector<edge> ans;
vector<int> mp[maxn]; 
int cnt=0;
void tarjan(int u,int fa){
	low[u]=dfn[u]=++cnt;
	for(int i=0;i<mp[u].size();i++){
		int v=mp[u][i];
		if(!dfn[v]){
			tarjan(v,u);
			low[u]=mini(low[u],low[v]);
			if(low[v]>dfn[u]){
				ans.push_back(u<v ? (edge){u,v} : (edge){v,u});
			}
		}
		else if(v!=fa){
			low[u]=mini(low[u],dfn[v]);
		}
	}
	return;
}
int main(){
	read(n),read(m);
	int u,v;
	for(int i=1;i<=m;i++){
		read(u),read(v);
		mp[u].push_back(v);
		mp[v].push_back(u); 
	}
	tarjan(1,1);
	sort(ans.begin(),ans.end(),cmp);
	for(int i=0;i<ans.size();i++){
		printf("%d %d\n",ans[i].u,ans[i].v);
	}
	return 0;
}

割点

额外注意的点就是根节点要特判,如果子树多于 \(1\) 颗,那么也是割点

我又只跑了一遍tarjan然后WA了

例题

#include<bits/stdc++.h>
#define usetime() (double)clock () / CLOCKS_PER_SEC * 1000.0
using namespace std;
typedef long long LL;
const int maxn=2e4+5,maxm=1e5+5;
void read(int& x){
	char c;
	bool f=0;
	while((c=getchar())<48) f|=(c==45);
	x=c-48;
	while((c=getchar())>47) x=x*10+c-48;
	x=(f ? -x : x);
	return;
}
class mmp{
public:
	int head[maxn],nxt[maxm<<1],e[maxm<<1];
	int mp_cnt;
	mmp(){
		memset(head,-1,sizeof(head));
		mp_cnt=-1;
	}
	void add_edge(int u,int v){
		e[++mp_cnt]=v;
		nxt[mp_cnt]=head[u];
		head[u]=mp_cnt;
	}
}mp;
int n,m,rt;
int dfn[maxn],low[maxn];
bool f[maxn];
vector<int> ans;
int tot=0;
void tarjan(int u,int fa){
	dfn[u]=low[u]=++tot;
	int cnt=0;
	for(int i=mp.head[u];~i;i=mp.nxt[i]){
		int v=mp.e[i];
		if(v==fa) continue;
		if(!dfn[v]){
			++cnt;
			tarjan(v,u);
			if(low[v]>=dfn[u]&&(!f[u])&&u!=rt){
				f[u]=1,ans.push_back(u);
			}
			low[u]=min(low[u],low[v]);
		}
		else{
			low[u]=min(low[u],dfn[v]);
		}
	}
	if(u==rt&&cnt>1){
		f[u]=1,ans.push_back(u);
	}
}
int main(){
	read(n),read(m);
	int u,v;
	for(int i=1;i<=m;i++){
		read(u),read(v);
		mp.add_edge(u,v),mp.add_edge(v,u);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]) rt=i,tarjan(i,i);
	}
	sort(ans.begin(),ans.end());
	printf("%d\n",(int)ans.size());
	for(int i=0;i<ans.size();i++){
		printf("%d ",ans[i]);
	}
	return 0;
}
//^o^

边双

边双和点双均可以先求割点割边再跑一遍\(dfs\)解决,此处提供单\(tarjan\)写法

求法和强连通分量类似,因为不需要考虑横叉边,所以就不用 \(ins\) 数组了

这道例题需要注意一下重边,因为有重边的话是可以返回父节点的

判断方式也很简单,用链式前向星判断是否和下来的边是反边来替代判断是否是父亲节点就好

#include<bits/stdc++.h>
#define usetime() (double)clock () / CLOCKS_PER_SEC * 1000.0
using namespace std;
typedef long long LL;
const int maxn=5e5+5,maxm=2e6+5;
void read(int& x){
	char c;
	bool f=0;
	while((c=getchar())<48) f|=(c==45);
	x=c-48;
	while((c=getchar())>47) x=(x<<3)+(x<<1)+c-48;
	x=(f ? -x : x);
	return;
}
class mmp{
public:
	int head[maxn],nxt[maxm<<1],e[maxm<<1];
	int mp_cnt;
	mmp(){
		memset(head,-1,sizeof(head));
		mp_cnt=-1;
	}
	void add_edge(int u,int v){
		e[++mp_cnt]=v;
		nxt[mp_cnt]=head[u];
		head[u]=mp_cnt;
	}
}mp;
int n,m;
int dfn[maxn],low[maxn];
vector<int> ans[maxn];
stack<int> st;
int tot=0,cnt=0;
void tarjan(int u,int ed){
	dfn[u]=low[u]=++tot;
	st.push(u);
	for(int i=mp.head[u];~i;i=mp.nxt[i]){
		int v=mp.e[i];
		if((i^1)==ed) continue;
		if(!dfn[v]){
			tarjan(v,i);
			low[u]=min(low[u],low[v]);
		}
		else{
			low[u]=min(low[u],dfn[v]);
		}
	}
	if(dfn[u]==low[u]){
		++cnt;
		int now=-1;
		while(now!=u){
			now=st.top(),st.pop();
			ans[cnt].push_back(now);
		}
	}
}
int main(){
	read(n),read(m);
	int u,v;
	for(int i=1;i<=m;i++){
		read(u),read(v);
		mp.add_edge(u,v),mp.add_edge(v,u);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]) tarjan(i,-1);
	}
	printf("%d\n",cnt);
	for(int i=1;i<=cnt;i++){
		printf("%d ",(int)ans[i].size());
		for(int j=0;j<ans[i].size();j++){
			printf("%d ",ans[i][j]);
		}
		printf("\n");
	}
	return 0;
}
//^o^

边双连通分量也可用于缩点,缩完以后会得到一颗树

点双

一个自环把我折腾死了

每个点双内\(dfs\)序最小的必定是割点

所以就是在割点的基础上加个栈罢了,这里之所以不 \(fa\) 记父亲节点

是因为现在单个点(就是那种删掉它图仍然连通的点)也要被算作割点,这样才能成为点双

我们要保证每一个点都在点双内

image

因为我们现在是这样添加点双的,所以每次只弹栈到其子节点,割点要保留

注意一个点是可以在多个点双内的,注意孤点的判断

#include<bits/stdc++.h>
#define usetime() (double)clock () / CLOCKS_PER_SEC * 1000.0
using namespace std;
typedef long long LL;
const int maxn=5e5+5,maxm=2e6+5;
void read(int& x){
	char c;
	bool f=0;
	while((c=getchar())<48) f|=(c==45);
	x=c-48;
	while((c=getchar())>47) x=(x<<3)+(x<<1)+c-48;
	x=(f ? -x : x);
	return;
}
class mmp{
public:
	int head[maxn],nxt[maxm<<1],e[maxm<<1];
	int mp_cnt;
	mmp(){
		memset(head,-1,sizeof(head));
		mp_cnt=-1;
	}
	void add_edge(int u,int v){
		e[++mp_cnt]=v;
		nxt[mp_cnt]=head[u];
		head[u]=mp_cnt;
	}
}mp;
int n,m,rt;
int dfn[maxn],low[maxn];
bool f[maxn];
vector<int> ans[maxn];
stack<int> st;
int tot=0,cnt=0;
void tarjan(int u){
	int son=0;
	dfn[u]=low[u]=++tot;
	st.push(u);
	if(mp.head[u]==-1&&u==rt){
		ans[++cnt].push_back(u);
	}
	for(int i=mp.head[u];~i;i=mp.nxt[i]){
		int v=mp.e[i];
		if(!dfn[v]){
			++son;
			tarjan(v);
			low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]){
				f[u]=1;
				++cnt;
				int now=-1;
				while(now!=v){
					now=st.top(),st.pop();
					ans[cnt].push_back(now);
				}
				ans[cnt].push_back(u);
			}
		}
		else{
			low[u]=min(low[u],dfn[v]);
		}
	}
}
int main(){
	read(n),read(m);
	int u,v;
	for(int i=1;i<=m;i++){
		read(u),read(v);
		if(u==v) continue;
		mp.add_edge(u,v),mp.add_edge(v,u);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i]) rt=i,tarjan(i);
	}
	printf("%d\n",cnt);
	for(int i=1;i<=cnt;i++){
		printf("%d ",(int)ans[i].size());
		for(int j=0;j<ans[i].size();j++){
			printf("%d ",ans[i][j]);
		}
		printf("\n");
	}
	return 0;
}
//^o^
posted @ 2025-08-15 19:55  huangems  阅读(25)  评论(0)    收藏  举报