【小结】tarjan

题单:
Tarjan大礼包

“超级点”就是极大联通子图。缩完之后变权值,DAG 上 DP。

SCC 的编号的倒叙就是拓扑序(注意不唯一)。

因为是按点的遍历顺序缩的。

所以有时可以直接倒叙 DP,不需要队列模拟。

然后 tarjan 的原理就是:

\(low_x\) \(x\) 能到达的最小时间戳。

\(dfn_x\) \(x\) 自己的时间戳。

记录时间戳。然后进入一个环的就是 \(low_x\)=\(dfn_x\)

\(dfn_x\) = \(low_x\),则此点为入口。

\(dfn_x\) = \(0\),那么没走过,直接\(low_x=\min(low_x,low_{to})\)

\(to\) 在栈里,则 \(low_x=\min(low_x,dfn_{to})\)
因为他是环的一部分,可以直接 \(dfn_{to}\),也可以 \(low_{to}\)。因为只要大小关系。

tarjan 本质上也是搜索树 + 返祖边,然后去判断。

P3387 【模板】缩点

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10,M=1e5+10;
int n,m,i,v[N],w[N],x[M],y[M],ans;
int idx,st[N],tp,low[N],dfn[N];
int last[N],c,cnt,id[N],dp[N],din[N];
bool in[N];
queue<int> q;
struct Node{
	int to,next;
}a[M];
void add(int u,int v)
{
	a[++c]={v,last[u]};
	last[u]=c;
}
void tarjan(int x)
{
	low[x]=dfn[x]=++idx;
	st[++tp]=x;
	in[x]=true;
	int i,to;
	for (i=last[x];i;i=a[i].next)
	{
		to=a[i].to;
		if (!dfn[to])
		{
			tarjan(to);
			low[x]=min(low[x],low[to]);
		}
		else if (in[to]) low[x]=min(low[x],dfn[to]);
	}
	if (low[x]==dfn[x])
	{
		cnt++;
		int t;
		do
		{
			t=st[tp];
			tp--;
			id[t]=cnt;
			w[cnt]+=v[t];
			in[t]=false;
		}while (t!=x);
	}
}
void topo()
{
	int i,j,x,to;
	for (i=cnt;i>=1;i--)
	{
		dp[i]=max(dp[i],w[i]);
		for (j=last[i];j;j=a[j].next)
		{
			to=a[j].to;
			dp[to]=max(dp[to],dp[i]+w[to]);
		}
		ans=max(ans,dp[i]);
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for (i=1;i<=n;i++) scanf("%d",&v[i]);
	for (i=1;i<=m;i++)
	{
		scanf("%d%d",&x[i],&y[i]);
		add(x[i],y[i]);
	}
	for (i=1;i<=n;i++)
	{
		if (!dfn[i]) tarjan(i);
	}
	for (i=1;i<=n;i++) last[i]=0;
	c=0;
	for (i=1;i<=m;i++)
	{
		if (id[x[i]]!=id[y[i]]) add(id[x[i]],id[y[i]]),din[id[y[i]]]++;
	}
	topo();
	printf("%d",ans);
	return 0;
}

然后就是双连通分量。

边双联通分量:删任意一条边还能联通(不包含桥)。

桥:一条边,删除后增加联通快。

tarjan。

\(dfn_x>low_{to}\),则此边隔开了两点,故此边为桥。

一般应用场景:缩点成为树。

P8436 【模板】边双连通分量

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10,M=2e6+10;
int n,m,i,x,y;
int last[N],c,ID[N],cnt;
int dfn[N],low[N],idx;
bool cut[M];
vector<int> G[N];
struct Node{
	int to,next,id;
}a[M<<1];
void add(int u,int v,int w)
{
	a[++c]={v,last[u],w};
	last[u]=c;
}
void tarjan(int x,int fa)
{
	int i,to,id;
	dfn[x]=low[x]=++idx;
	for (i=last[x];i;i=a[i].next)
	{
		to=a[i].to;
		id=a[i].id;
		if (!dfn[to])
		{
			tarjan(to,id);
			low[x]=min(low[x],low[to]);
			if (low[to]>dfn[x]) cut[id]=true;
		}
		else if (id!=fa) low[x]=min(low[x],dfn[to]);
	}
}
void dfs(int x)
{
	int i,to,id;
	ID[x]=cnt;
	for (i=last[x];i;i=a[i].next)
	{
		to=a[i].to;
		id=a[i].id;
		if (ID[to]||cut[id]) continue;
		dfs(to);
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for (i=1;i<=m;i++)
	{
		scanf("%d%d",&x,&y);
		add(x,y,i);
		add(y,x,i);
	}
	for (i=1;i<=n;i++)
	{
		if (!dfn[i]) tarjan(i,0);
	}
	for (i=1;i<=n;i++)
	{
		if (!ID[i])
		{
			cnt++;
			dfs(i);
		}
	}
	for (i=1;i<=n;i++) G[ID[i]].push_back(i);
	printf("%d\n",cnt);
	for (i=1;i<=cnt;i++)
	{
		printf("%d ",G[i].size());
		for (int x:G[i]) printf("%d ",x);
		printf("\n");
	}
	return 0;
}

点双联通分量:

就是不含割点的极大子图。

性质:

  1. 除了两点一线,点双都是边双。

  2. 一个割点一定在 2 个以上(含 2 个)点双里。

  3. 没有割点 <-> 图联通。

因为删除一个点就是删除多条边。

割点:删除此点后,联通快数量增加。

求割点:tarjan。当 \(dfn_x \leq low_{to}\),此点拦住了 \(to\)

P3388 【模板】割点(割顶)

#include<bits/stdc++.h>
using namespace std;
const int N=2e4+10,M=1e5+10;
int n,m,i,x,y,ans[N],k;
int last[N],c;
int dfn[N],low[N],idx;
bool cut[N];
struct Node{
	int to,next;
}a[M<<1];
void add(int u,int v)
{
	a[++c]={v,last[u]};
	last[u]=c;
}
void tarjan(int root,int x)
{
	int i,to,cnt=0;
	dfn[x]=low[x]=++idx;
	for (i=last[x];i;i=a[i].next)
	{
		to=a[i].to;
		if (!dfn[to]) 
		{
			cnt++;
			tarjan(root,to);
			low[x]=min(low[x],low[to]);
			if (low[to]>=dfn[x]) 
			{
				if (x!=root||cnt>=2) cut[x]=true;
			}
		}
		else low[x]=min(low[x],dfn[to]);
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for (i=1;i<=m;i++)
	{
		scanf("%d%d",&x,&y);
		add(x,y);
		add(y,x);
	}
	for (i=1;i<=n;i++)
	{
		if (!dfn[i]) tarjan(i,i);
	}
	for (i=1;i<=n;i++)
	{
		if (cut[i]) ans[++k]=i;
	}
	printf("%d\n",k);
	for (i=1;i<=k;i++) printf("%d ",ans[i]);
	return 0;
}

还要考虑两点一线的情况。

\(x=root\)\(son=1\) 时,此点为两点一线,排除掉即可。

然后其他都差不多。就是记录栈 \(st[]\),然后到一个\(类\)割点的时候,就可以弹栈。

易错点:

  1. 不能弹到 \(x\),这样会多弹子树。

  2. 不需要特判,因为根节点也满足。

  3. 弹完要把\(类\)割点 \(x\) 放入。

P8435 【模板】点双连通分量

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10,M=2e6+10;
int n,m,i,x,y;
int last[N],c;
int dfn[N],low[N],idx,cnt;
stack<int> st;
vector<int> G[N];
struct Node{
	int to,next;
}a[M<<1];
void add(int u,int v)
{
	a[++c]={v,last[u]};
	last[u]=c;
}
void tarjan(int x)
{
	if (!last[x])
	{
		G[++cnt].push_back(x);
		return;
	}
	dfn[x]=low[x]=++idx;
	st.push(x);
	int i,to;
	for (i=last[x];i;i=a[i].next)
	{
		to=a[i].to;
		if (!dfn[to])
		{
			tarjan(to);
			low[x]=min(low[x],low[to]);
			if (low[to]>=dfn[x])
			{
				int tp;
				cnt++;
				while (!st.empty())
				{
					tp=st.top();
					st.pop();
					G[cnt].push_back(tp);
					if (tp==to) break;
				}
				G[cnt].push_back(x);
			}
		}
		else low[x]=min(low[x],dfn[to]);
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for (i=1;i<=m;i++)
	{
		scanf("%d%d",&x,&y);
		if (x==y) continue;
		add(x,y);
		add(y,x);
	}
	for (i=1;i<=n;i++)
	{
		if (!dfn[i]) tarjan(i);
	}
	printf("%d\n",cnt);
	for (i=1;i<=cnt;i++)
	{
		printf("%d ",G[i].size());
		for (int x:G[i]) printf("%d ",x);
		printf("\n");
	}
	return 0;
}

2-SAT

就是一种经典 tarjan。

模型就是给定 \(n\) 个 bool 变量(两种选择),要满足 \(m\) 个条件:\(a_i=x\)\(a_j=y\)

\(i\)\(a_i\) 为 0 的点 ,\(i+n\)\(a_i\) 为 1 的点。

那么问题就转化成了 \(a_i!=x\)->\(a_j=y\)\(a_j!=y\)->\(a_i=x\)

那么照着这个直接建模就行。

注意最后的构造:为什么从拓扑后往前?为什么一定有解?

因为 \(i\)\(i+n\) 如果不在一条路径上,那么就没关系。

在一条路径上,那么如果在同一个强连通分量里,那么无解。否则我选路径上较后面的点,一定成立。

P4782 【模板】2-SAT

#include<bits/stdc++.h>
using namespace std;
/*
另 i 表示 i 为 true,i+n 表示 i 为 false。
u->v u 推出 v。 
*/
const int N=3e6+10,M=3e6+10;
int n,m,x,y,aa,bb,i;
int low[N],dfn[N],idx,cnt,id[N];
int last[N],c;
int st[N],tp;
bool in[N]; 
struct Node{
	int to,next;
}a[M<<1]; 
void add(int u,int v)
{
	a[++c]={v,last[u]};
	last[u]=c;
}
void tarjan(int x)
{
	low[x]=dfn[x]=++idx;
	st[++tp]=x;
	in[x]=true;
	int i,to;
	for (i=last[x];i;i=a[i].next)
	{
		to=a[i].to;
		if (!dfn[to])
		{
			tarjan(to);
			low[x]=min(low[x],low[to]);
		}
		else if (in[to]) low[x]=min(low[x],dfn[to]);
	}
	if (low[x]==dfn[x])
	{
		cnt++;
		int t;
		do
		{
			t=st[tp];
			tp--;
			id[t]=cnt;
			in[t]=false;
		}while (t!=x);
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for (i=1;i<=m;i++)
	{
		scanf("%d%d%d%d",&x,&aa,&y,&bb);
		if (!aa&&!bb) add(x+n,y),add(y+n,x);
		else if (!aa&&bb) add(x+n,y+n),add(y,x);
		else if (aa&&!bb) add(x,y),add(y+n,x+n);
		else add(x,y+n),add(y,x+n);
	}
	for (i=1;i<=2*n;i++)
	{
		if (!dfn[i]) tarjan(i);
	}
	for (i=1;i<=n;i++)
	{
		if (id[i]==id[i+n])
		{
			printf("IMPOSSIBLE\n");
			return 0;
		}
	}
	printf("POSSIBLE\n");
	for (i=1;i<=n;i++) printf("%d ",id[i]>id[i+n]);
	return 0;
}
posted @ 2026-08-21 17:19  GUO120822  阅读(3)  评论(0)    收藏  举报