图的连通性相关(Tarjan算法)

(大抄蓝书)

Part 1:无向图连通性

无向图的割点与桥

给定无向图 G=(V,E)

  • 若对于 xV,从图中删去节点 x 以及所有与 x 关联的边之后,G 分裂成两个或两个以上不相连的子图,则称 xG割点

  • 若对于 eE,从图中删去边 e 之后,G 分裂成两个不相连的子图,则称 eG割边

Tarjan 算法基于无向图的深度优先遍历,能够在线性时间内求出无向图的割点和桥,进一步可求出无向图的双联通分量。下面给出一些定义及定理:

时间戳

在图的深度优先遍历中,按照每个节点第一次被访问的时间顺序,给予 1n 的整数标记,该标记就被称为“时间戳”,记为 dfn[x]

搜索树

在无向联通图中任选一个节点进行深度优先遍历,每个点只访问一次,所以发生递归的边 (x,y) 构成一棵树,这棵树被称为“无向联通图的搜索树”。

若无向图不连通,则各个连通块的搜索树构成无向图的“搜索森林”

追溯值

subtree(x) 表示搜索树中以 x 为根的子树。x 的追溯值 low[x] 定义为以下节点的时间戳的最小值

  • subtree(x) 中的节点
  • 通过 1 条不在搜索树上的边,能够达到 subtree(x) 的节点

感性理解为不经过其父亲能到达的最小的时间戳。

割边判定法则

无向图 (x,y) 是桥,当且仅党搜索树上存在一个 x 的子节点 y,满足:

dfn[x]<low[y]

同时可以得出一些结论:

  • 桥一定是搜索树中的边

  • 一个简单环中的边一定都不是桥

一道割边模板题 P1656 炸铁路

考虑一个细节:

对于边 (x,fa) 来说,如果无重边,fa 的时间戳不能更新 low[x]。但如果有重边就可以。所以不能单纯在递归时单纯记录每个节点的父节点。我们可以使用成对变换技巧,沿着编号 i 的边进入节点 x,则忽略从 x 出发的编号为 ixor1 的边

#include<bits/stdc++.h>
using namespace std;

const int N=250,M=5010;

int n,m,dfn[N],low[N],num,cnt;
int head[N],ver[2*M],nxt[2*M],tot=1;
pair <int,int> ans[M];
bool bri[2*M];

bool cmp(pair<int,int> a,pair<int,int> b)
{
	if(a.first==b.first)
		return a.second<b.second;
	return a.first<b.first;
}

void add(int x,int y)
{
	ver[++tot]=y;
	nxt[tot]=head[x];
	head[x]=tot;
}

void tarjan(int x,int last)
{
	low[x]=dfn[x]=++num;
	
	for(int i=head[x]; i; i=nxt[i])
	{
		int y=ver[i];
		
		if(!dfn[y])
		{
			tarjan(y,i);
			low[x]=min(low[x],low[y]);
			if(dfn[x]<low[y]) //割边判定法则
				bri[i]=bri[i^1]=1;		
		}
		else if(i!=(last^1))
			low[x]=min(low[x],dfn[y]);
	}
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1; i<=m; i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		add(x,y);  add(y,x);
	}
	
	for(int i=1; i<=n; i++)
		if(!dfn[i])
			tarjan(i,0);
			
	for(int i=2; i<=tot; i+=2)
	{
		if(bri[i])
		{
			int x=ver[i],y=ver[i^1];
			if(x>y)
				swap(x,y);
			ans[++cnt]=make_pair(x,y);
		}
	}
	
	sort(ans+1,ans+1+cnt,cmp);
	
	for(int i=1; i<=cnt; i++)
		printf("%d %d\n",ans[i].first,ans[i].second);
	
	return 0;
}

割点判定法则

x 不是搜索树的根节点,则 x 是割点当且仅当搜索树上存在一个 x 的子节点 y,满足:

dfn[x]low[y]

特别地,若 x 是搜索树的根节点,则 x 是割点当且仅当搜索树上存在至少两个子节点 y1,y2 满足上述条件

一道割点模板题【模板】割点(割顶)

因为割点判定法则是小于等于号,所以在求割点时,不必考虑父节点和重边的问题,从 x 出发能访问到的所有节点的时间戳都能用来更新 low[x]

#include<bits/stdc++.h>
using namespace std;

const int N=20010,M=100010;

int n,m,dfn[N],low[N],num,rt;
int head[N],ver[2*M],nxt[2*M],tot;
bool cut[N];

void add(int x,int y)
{
	ver[++tot]=y;
	nxt[tot]=head[x];
	head[x]=tot;
}

void tarjan(int x)
{
	dfn[x]=low[x]=++num;
	int t=0;
	
	for(int i=head[x]; i; i=nxt[i])
	{
		int y=ver[i];
		if(!dfn[y])
		{
			tarjan(y);
			low[x]=min(low[x],low[y]);
			
			if(dfn[x]<=low[y]) //割点判定法则 
			{
				t++;
				if(x!=rt || t>1)
					cut[x]=1;
			}
		}
		else
			low[x]=min(low[x],dfn[y]);
	}
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1; i<=m; i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		add(x,y);  add(y,x);
	}
	
	for(int i=1; i<=n; i++)
		if(!dfn[i])
			rt=i,tarjan(i);
	
	int cnt=0;
	for(int i=1; i<=n; i++)
		if(cut[i])
			cnt++;
	
	printf("%d\n",cnt);
	for(int i=1; i<=n; i++)
		if(cut[i])
			printf("%d ",i);
	
	return 0;
}

无向图的双联通分量

定义

  • 若一张无向联通图不存在割点,则称它为“点双连通图”。若一张无向联通图不存在桥,则称它为“边双联通图”

  • 无向图的极大点双联通子图被称为“点双联通分量”,简记为“vDCC”。无向图的极大边双联通子图被称为“边双连通分量”,简记为“eDCC”。二者统称为“双连通分量”,简记为“DCC

定理

一张无向联通图是“点双联通图”,当且仅当满足下列两个条件之一:

  • 图中的顶点数不超过 2

  • 图中任意两点都同时包含在至少一个简单环中。其中“简单环”指的是不自交的环

定理

一张无向联通图是“边双连通图”,当且仅当任意一条边都包含在至少一个简单环中

边双(e-DCC)的求法

边双连通分量的计算非常简单,只需求出无向图中所有的桥,把桥都删除后,剩余的若干个连通块就是若干个“边双连通分量”

实现时,可以先用 Tarjan 标记出所有桥边,再对整个图进行一次深度优先遍历(不访问桥边),用 c 数组给每个节点 x 编号即可

模板题

int c[N],dcc;

void dfs(int x)
{
	c[x]=dcc;
	for(int i=head[x]; i; i=nxt[i])
	{
		int y=ver[i];
		if(c[y] || bri[i])
			continue;
		dfs(y);
	}
}

//在main函数中
for(int i=1; i<=n; i++)
	if(!c[i])
		dcc++,dfs(i);

边双(e-DCC)的缩点

把每个 eDCC 看成一个点,把桥边 (x,y) 看作连接编号为 c[x]c[y]eDCC 对应节点的无向边,产生一棵树或森林。这就是 eDCC 的缩点

int hc[N],vc[2*N],nc[2*N],tc=1;

void add_c(int x,int y)
{
	vc[++tc]=y;
	nc[tc]=x;
	hc[x]=tc;
}

//在main函数中
for(int i=2; i<=tot; i+=2)
{
	int x=ver[i^1],y=ver[i];
	if(c[x]==c[y])
		continue;
	add_c(c[x],c[y]);
}

点双(v-DCC)

求解点双联通分量一般使用圆方树算法,它基于 Tarjan 算法,但实现略有不同

在圆方树算法中,每次会找到一个点双在搜索树上的根节点(也是割点),此时有 dfn[x]=low[y],之后将点双标记成一个方点,将原来点双内的点与方点依次连接,建成一棵树。

在这棵树内,有原点(1n),方点(>n),故称圆方树

cnt=n;

void tarjan(int x)
{
	dfn[x]=low[x]=++dfn[0]; 
	sta[++top]=x;
	for(int i=head[x]; i; i=nxt[i])
	{
		int y=ver[i];
		if(!dfn[y])
		{
			tarjan(y);
			low[x]=min(low[x],low[y]);
			if(dfn[x]==low[y])
			{
				cnt++;
				int z;
				do
				{
					z=sta[top--];
					g[cnt].push_back(z);
					g[z].push_back(cnt);
				}while(z!=y);
				g[cnt].push_back(x);
				g[x].push_back(cnt);
			}
		}
		else
			low[x]=min(low[x],dfn[y]);
	}
}

Part 2:有向图连通性

前置知识

给定一个有向图 G=(V,E),若存在 rV,满足从 r 出发能够到达 V 中的所有点,则称 G 是一个“流图”,记为 (G,r),其中 r 称为流图的源点

在一个流图 (G,r) 上从 r 出发进行深度优先遍历,每个点只访问一次,所有发生递归的边 (x,y) 构成一棵以 r 为根的树,我们把它称为流图 (G,r)搜索树

同时,类似无向图,在流图的深度优先遍历中,按照每个节点第一次被访问的时间顺序,给予 1n 的整数标记,该标记被称为“时间戳”,记为 dfn[x]

流图中的每条有向边 (x,y) 必然是以下四种之一:

  • 树边:指搜索树中的边,即 xy 的父亲
  • 前向边:指搜索树中 xy 的祖先
  • 返祖边:指搜索树中 yx 的祖先
  • 横叉边:除了上述三种情况以外的边,它一定满足 dfn[y]<dfn[x]

有向图的强联通分量

  • 在有向图中,若两个节点 x,y 能够相互到达,则称这两个点是强连通的,它们之间具有强连通性

  • 给定一张有向图,若对于图中任意两个节点 x,y 都是强联通的,则称该有向图是一张强连通图

  • 有向图的极大强联通子图被称为“强连通分量”,简记为“SCC

Tarjan 算法基于有向图的深度优先遍历,能够在线性时间内求出一张有向图的各个强联通分量。

一个“环”一定是强联通图。如果节点 x,y 强联通,那么 x,y 显然在一个环中。因此,Tarjan 算法的基本思路就是对于每个点,尽量找到与它一起能构成环的所有节点

容易发现,前向边没有什么用处,而返祖边非常有用,横叉边可能有用。为了找到通过“返祖边”和“横叉边”构成的环,Tarjan 算法在深度优先遍历的同时维护了一个栈。当访问到节点 x 时,栈中需要保存以下两类节点:

  • 搜索树上 x 的祖先节点,记为集合 anc(x)

    yanc(x)。若存在返祖边 (x,y),则 (x,y)yx 的路径一起构成环

  • 已经访问过,并且存在一条路径到达 anc(x) 的节点

    z 是这样的一个点,从 z 出发存在一条路径到 tanc(x)。若存在横叉边 (x,z),则 (x,z)zy 的路径,yx 的路径构成一个环

综上,栈中的节点就是能与 x 出发的“返祖边”、“横叉边”形成环的节点。进而可以引入追溯值的概念

追溯值

subtree(x) 表示流图的搜索树中以 x 为根的子树。x 的追溯值 low[x] 定义为满足以下条件的节点的最小时间戳:

  • 该点在栈中
  • 存在一条从 subtree(x) 出发的有向边,以该点为终点

根据定义,可按照以下步骤计算“追溯值”

  • 当节点 x 第一次被访问时,把 x 入栈,初始化 low[x]=dfn[x]

  • 扫描从 x 出发的每条边 (x,y)

    • y 没被访问过,说明 (x,y) 是树边,递归访问 y,从 y 回溯以后,令 low[x]=min(low[x],low[y])
    • y 被访问过且 y 在栈中,则令 low[x]=min(low[x],dfn[y])
  • x 回溯之前,判断是否有 low[x]=dfn[x]。若成立,则不断地从栈中弹出节点,直至 x 出栈

强连通分量判定法则

在追溯值的计算过程中,若在 x 回溯前,有 low[x]=dfn[x] 成立,则栈中从 x 到栈顶的所有节点构成一个强联通分量

SCC的缩点

eDCC 的缩点类似,对于每条有向边 (x,y),若 c[x]c[y],则在编号为 c[x]c[y]SCC 之间连边,最后会得到一张有向无环图

SCC 缩点模板题【模板】缩点

#include<bits/stdc++.h>
using namespace std;

const int N=10010,M=100010;

int n,m,a[N],mx;
int dfn[N],low[N],num;
int sta[N],top,ins[N],c[N],cnt,val[N];
int ans[N],p,in_deg[N],f[N];
int head[N],ver[M],from[M],nxt[M],tot;
vector <int> scc[N],g[N],gg[N];
queue <int> q;

void add(int x,int y)
{
	ver[++tot]=y;
	from[tot]=x;
	nxt[tot]=head[x];
	head[x]=tot;
}

void tarjan(int x)
{
	dfn[x]=low[x]=++num;
	sta[++top]=x;  ins[x]=1;
	
	for(int i=head[x]; i; i=nxt[i])
	{
		int y=ver[i];
		if(!dfn[y])
		{
			tarjan(y);
			low[x]=min(low[x],low[y]);
		}
		else if(ins[y])
			low[x]=min(low[x],dfn[y]);
	}
	
	if(dfn[x]==low[x])
	{
		cnt++;  int y;
		do
		{
			y=sta[top--];  ins[y]=0;
			c[y]=cnt;  
			scc[cnt].push_back(y);
			val[cnt]+=a[y];
		}while(x!=y);
	}
}

void topsort()
{
	for(int i=1; i<=cnt; i++)
		if(!in_deg[i])
			q.push(i);
	
	while(q.size())
	{
		int x=q.front();  q.pop();
		ans[++p]=x;
		
		for(int i=0; i<g[x].size(); i++)
		{
			int y=g[x][i];
			in_deg[y]--;
			if(in_deg[y]==0)
				q.push(y);
		}
	}
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1; i<=n; i++)
		scanf("%d",&a[i]);
	for(int i=1; i<=m; i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		add(x,y);
	}
	
	for(int i=1; i<=n; i++)
		if(!dfn[i])
			tarjan(i);

	for(int i=1; i<=tot; i++)
	{
		int x=from[i],y=ver[i];
		if(c[x]==c[y])
			continue;
		g[c[x]].push_back(c[y]);
		gg[c[y]].push_back(c[x]);
		in_deg[c[y]]++;
	}
	
	topsort();
	
	for(int i=1; i<=cnt; i++)
	{
		int x=ans[i];
		f[x]=val[x];
		
		for(int j=0; j<gg[x].size(); j++)
			f[x]=max(f[x],f[gg[x][j]]+val[x]);
		mx=max(mx,f[x]);
	}
	
	printf("%d",mx);
	
	return 0;
}
posted @   xishanmeigao  阅读(48)  评论(0编辑  收藏  举报
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示