Tarjan算法 —— 强连通双连通缩点 模板

有向图

我们知道在一张 有向无环 图(也叫 DAG)中,肯定存在拓扑序。拓扑序的特殊顺序性质,能够允许我们在 O ( n + m ) O(n + m) O(n+m) 遍历这张图,还可以在图上用 dp 处理问题。

但大部分给的 有向图 都不是 DAG,用 Tarjan 算法,我们可以尝试转化,把环缩成点 —— 强连通分量。

在任意一个强连通分量中,内部的点两两之间都可以通过有向边到达

预处理缩点的时间是 O ( n + m ) O(n + m) O(n+m)

无向图

无向图本身两点之间就可以成环,所以没有环的说法。

分为两种:

  1. 边的双连通分量
  2. 点的双连通分量

在任意一个边双连通分量中,删去任意一条边不会使图不连通。

在任意一个点双连通分量中,删去任意一个点不会使图不连通;

双连通缩点后的图都长得像一棵树

强连通缩点模板

针对有向图

int h[N], rh[N], e[M], re[N], ne[M], idx;
void add(int h[], int a, int b) {
	e[idx] = b, re[idx] = a, ne[idx] = h[a], h[a] = idx++;
}
// Tarjan模板

int dfn[N], low[N], tim;
int stk[N], top;
bool v[N];
int scc_cnt, id[N], siz[N];
//分量个数,属于哪个分量,分量大小

void tarjan(int x) {
	dfn[x] = low[x] = ++tim;
	stk[++top] = x;
	v[x] = true;
	for (int i = h[x]; ~i; i = ne[i]) {
		int j = e[i];
		if (!dfn[j]) {
			tarjan(j);
			low[x] = min(low[x], low[j]);
		}
		else if (v[j])low[x] = min(low[x], dfn[j]);
		//也在栈中
	}

	if (dfn[x] == low[x]) {
		int y;
		scc_cnt++;
		do
		{
			y = stk[top--];
			v[y] = false;
			id[y] = scc_cnt;
			siz[scc_cnt]++;
		} while (y != x);
	}
}
	for (int i = 1; i <= n; i++)
		if (!dfn[i])tarjan(i);
	//缩点后重建图模板,去重

	unordered_map<ll, int> mp;
	for (int i = 0; i < idx; i++) {
		int b = e[i], a = re[i];
		
		//二维压成一维
		ll hash = a * N + b;
		if (id[b] != id[a] && !mp.count(hash)) {
			mp[hash] = 1;
			add(rh, a, b);
		}
	}
	//按拓扑序遍历图,直接反向遍历scc_cnt即可,无需记录入度
	
	for (int i = scc_cnt; i; i--) {
		//起点初始
		for (int g = rh[i]; ~g; g = ne[g]) {
			int j = e[g];
			//维护信息
		}
	}

双连通缩点模板

针对无向图

边双连通

int dfn[N], low[N], tim;
int stk[N], top;
bool is_bridge[M];
int dcc_cnt, id[N], siz[N];
//分量个数,属于哪个分量,分量大小

void tarjan(int x, int from) {
	dfn[x] = low[x] = ++tim;
	stk[++top] = x;
	//双连通不需要维护 v
	for (int i = h[x]; ~i; i = ne[i]) {
		int j = e[i];
		if (!dfn[j]) {
			tarjan(j, i);
			low[x] = min(low[x], low[j]);
			//记录桥
			if (dfn[x] < low[j])
				is_bridge[i] = is_bridge[i ^ 1] = true;
		}
		else if (i != (from ^ 1))low[x] = min(low[x], dfn[j]);
		//防止跟父节点更新就行
	}

	if (dfn[x] == low[x]) {
		int y;
		dcc_cnt++;
		do
		{
			y = stk[top--];
			id[y] = dcc_cnt;
		} while (y != x);
	}
}

点双连通

记录起来较为麻烦,需要特判根节点。

int dfn[N], low[N], tim;
int stk[N], top;
int dcc_cnt, root;
vector<int> dcc[N];//存储每个分量里有哪些点
bool cut[N];//记录割点

void tarjan(int x) {
	dfn[x] = low[x] = ++tim;
	stk[++top] = x;

	if (x == root && h[x] == -1) { //如果只有单独一个点
		dcc_cnt++;//单独一个分量
		dcc[dcc_cnt].push_back(x);
		return;
	}

	int cnt = 0;//记录该点连接的割点个数
	for (int i = h[x]; ~i; i = ne[i]) {
		int j = e[i];
		if (!dfn[j]) {
			tarjan(j);
			low[x] = min(low[x], low[j]);
			if (dfn[x] <= low[j]) { 

				cnt++;
				if (x != root || cnt > 1)cut[x] = true;
				//不是根节点时,因为肯定存在父节点,删去 x 分离了父节点与子节点
				//是根节点时,就要保证有两个子节点连到 x 上
				//这样才能说明 x 是割点

				dcc_cnt++;
				int y;
				do
				{
					y = stk[top--];
					dcc[dcc_cnt].push_back(y);
				} while (y != j);//到 j 就停止!

				dcc[dcc_cnt].push_back(x);//割点也放入
				//所以点双连通分量是有交集的
			}
		}
		else low[x] = min(low[x], dfn[j]);
		//点双连通可以随意取min
	}
}
	for (root = 1; root <= n; root++)
		if (!dfn[root])
			tarjan(root);

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值