【知识点复习】Tarjan 算法 与 图的连通性

前言

不知道为什么,以前特别喜欢 \(tarjan\)

不知道为什么,现在特别讨厌 \(tarjan\).

可能是写复习博写麻了吧。。

不过这确实是个复习的好方法(对于我这种又懒又摆的菜鸡而言)

为什么要把有向图无向图分开写……好麻烦

所以合到一起啦!

参考链接:

介绍

几个重要概念:

  • 时间戳(\(dfn\)):在 \(DFS\) 中,每个节点第一次被访问的时间顺序标号。

  • 搜索树:在无向图中任选一个点 \(DFS\),每个点只访问一次,所有发生递归的边构成的树。

  • 追溯值(\(low\)):记录该点属于的强连通分量。

  • 强连通:两个顶点 \(u\)\(v\) 可以相互到达

  • 强连通图:图中的任意两个顶点可以相互到达(就是图中有一块边缠在一起)

  • 强连通分量(\(scc\)):图 \(G\) 不是一个强连通图,但其子图 \(G^\prime\)\(G\)最大强连通子图,则称 \(G^\prime\) 为一个强连通分量

割点与桥

割点: 对于图 \(G\) 上任意一点 \(x\),从图中删去节点 \(x\) 以及所有与 \(x\) 关联的边之后, \(G\) 分裂成两个或两个以上不相连的子图,则称 \(x\)\(G\) 的割点;

桥(割边):对于图 \(G\) 上任意一边 \(e\),从图中删去边 \(e\) 之后, \(G\) 分裂成两个不相连的子图,则称 \(e\)\(G\) 的桥或割边;

桥的性质桥一定是搜索树中的边,并且一个简单环中的边一定都不是桥。

割边判定定则:无向边 (\(x, y\))是桥,当且仅当搜索树上存在 \(x\) 的一个子节点 \(y\),满足 \(dfn_x < low_y\)

割点判定定理:① \(v\)\(u\) 搜索树上的一个儿子 且 \(dfn_u <= low_v\)​,即 \(v\) 的子树中没有返祖边能跨越 \(u\) 点;② 有多个儿子

原理

通过 \(DFS\) 标记每个点的 \(dfn\)\(low\),有一个栈存下遍历过的点并维护每个点的 \(low\) 值来求得强连通分量。

具体过程图解参见参考链接里的。

模板


void tarjan(int x) {
  dfn[x] = low[x] = ++tim, vis[x] = 1, sta[++top] = x;
  int ver, siz = 0;
  for(int i = head[x]; i; i = nex[i]) {
  	ver = to[i];
  	if(!dfn[ver]) {
  		tarjan(ver, i);
  		low[x] = min(low[x], low[ver]);
  		if(low[ver] >= dfn[x]) cutnode[x] = 1; 
		if(low[ver] > low[x]) cutedge[f] = cutedge[f ^ 1] = 1;// 判断是否是桥(割边) 
		siz ++;	
	}
	else if(vis[ver]) low[x] = min(low[x], dfn[ver]);
  } 
  if(x == rt && siz > 1) cutnode[x] = 1;
  if(dfn[x] == low[x]) {
  	cnt ++;
  	while(sta[top] != x) {
  		bel[sta[top]] = cnt;
  		vis[sta[top]] = 0;
  		top --;
	}
	bel[x] = cnt, vis[x] = 0;
  }
}

int main() {
	for(int i = 1; i <= m; i ++) add(),…; 
	for(int i = 1; i <= n; i ++) {
    	if(!dfn[i]) tarjan(i);  
	}

}

应用

缩点

其实就是 \(Tarjan\) 过程中找到一个强连通分量,然后标号的过程。

if(dfn[x] == low[x]) {
  	cnt ++;
  	while(sta[top] != x) {
  		bel[sta[top]] = cnt;
  		vis[sta[top]] = 0;
  		top --;
	}
	bel[x] = cnt, vis[x] = 0;
  }

一般多与并查集连用。。

欧拉路问题(无向图连通性 跟 \(tarjan\) 无关)

定义

  • 欧拉路:给定一个无向图,若存在一条从节点 \(S\)\(T\) 的路径,恰好不重不漏地经过每条边一次(可以重复经过图中的节点),则称该路径为 \(S\)\(T\)欧拉路

  • 欧拉回路:给定一个无向图,若存在一条从节点 \(S\) 出发的路径,恰好不重不漏地经过每条边一次(可以重复经过图中的节点),最终回到起点 \(S\),则称该路径为 \(S\)\(T\)欧拉回路

  • 欧拉图:存在欧拉回路的无向图。

判定

  • 欧拉图:一张无向图为欧拉图,当且仅当该无向图连通,并且每个点的度数都是偶数;

  • 欧拉路:一张无向图存在欧拉路,当且仅当无向图连通,并且图中恰好有两个节点的度数为奇数,其他节点的度数都是偶数。这两个奇度数节点就是欧拉路的起点 \(S\) 和终点 \(T\)

代码

模板题欧拉路径

贴的洛谷题解区的(原因是因为我发现自己这道题 WA 掉了呢)

#include <bits/stdc++.h>
using namespace std;
const int MAX=100010;
int n,m,u,v,del[MAX];
int du[MAX][2];//记录入度和出度 
stack <int> st;
vector <int> G[MAX];
void dfs(int now)
{
	for(int i=del[now];i<G[now].size();i=del[now])
	{ 
		del[now]=i+1;
		dfs(G[now][i]);
	}
	st.push(now);
}
int main()
{
	scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++) scanf("%d%d",&u,&v),G[u].push_back(v),du[u][1]++,du[v][0]++;  
    for(int i=1;i<=n;i++) sort(G[i].begin(),G[i].end());
    int S=1,cnt[2]={0,0}; //记录
    bool flag=1; //flag=1表示,所有的节点的入度都等于出度,
    for(int i=1;i<=n;i++)
	{
        if(du[i][1]!=du[i][0]) flag=0;
        if(du[i][1]-du[i][0]==1/*出度比入度多1*/) cnt[1]++,S=i;
        if(du[i][0]-du[i][1]==1/*入度比出度多1*/) cnt[0]++;
    }
    if((!flag)&&!(cnt[0]==cnt[1]&&cnt[0]==1)) return !printf("No");
	//不满足欧拉回路的判定条件,也不满足欧拉路径的判定条件,直接输出"No" 
    dfs(S);
    while(!st.empty()) printf("%d ",st.top()),st.pop();
    return 0; 
}

练习

B城

题目传送门

B 城有 n 个城镇,m 条双向道路。

每条道路连结两个不同的城镇,没有重复的道路,所有城镇连通。

把城镇看作节点,把道路看作边,容易发现,整个城市构成了一个无向图。
输出共 n 行,每行输出一个整数。

第 i 行输出的整数表示把与节点 i 关联的所有边去掉以后(不去掉节点 i 本身),无向图有多少个有序点 (x,y),满足 x 和 y 不连通。

点击查看代码
#include<cstdio>
#include<queue>
#include<algorithm>
using namespace std;
#define int long long
const int N = 1e5 + 5, M = 5e5 + 5;
int n, m, tot, head[N], nex[M << 1], to[M << 1], ans[N];
void add(int x, int y) {
	to[++tot] = y, nex[tot] = head[x], head[x] = tot;
}
int tim, top, sta[N], siz[N], low[N], dfn[N];
bool vis[N];
void tarjan(int x) {
	dfn[x] = ++tim, low[x] = dfn[x], siz[x] = 1;
	int res = 0, cnt, ver;
	for(int i = head[x]; i; i = nex[i]) {
		ver = to[i];
		if(!dfn[ver]) {
			tarjan(ver);
			low[x] = min(low[x], low[ver]);
			siz[x] += siz[ver];
			if(dfn[x] <= low[ver]) {
				res += siz[ver], cnt ++;
				while(!top && sta[top] != x) top --;
				top--;
				ans[x] += (n - siz[ver]) * siz[ver];
			}
		}
		else low[x] = min(low[x], dfn[ver]);
	}
	if(!(x != 1 || cnt > 1)) ans[x] = 2 * n - 2;
	else ans[x] += (n - res - 1) * (res + 1) + n - 1; 
}

signed main() {
	scanf("%lld %lld", &n, &m);
	int u, v;
	for(int i = 1; i <= m; i ++) {
		scanf("%lld %lld", &u, &v);
		add(u, v), add(v, u);
	}
	tarjan(1);
	for(int i = 1; i <= n; i ++) printf("%lld\n", ans[i]);
	return 0;
} 

网络

题目传送门

给定一张 N 个点 M 条边的无向连通图,然后执行 Q 次操作,每次向图中添加一条边,并且询问当前无向图中“桥”的数量。

这题看起来挺简单的,其实还有点麻烦……

点击查看代码
#include<cstdio>
#include<cstring>
#include<queue>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 5, M = 4e5 + 5;

bool vis[M];
int n, m, Q, T, ans, f[N][20], fa[N], dep[N];
int tim, cnt, dfn[N], low[N], bel[N];
int t, hb[N], nb[M], tb[M];
int tot, head[N], nex[M], to[M];

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

void bdd(int x, int y) {
	tb[++t] = y, nb[t] = hb[x], hb[x] = t;
}

void tarjan(int x, int f = 0) {
	dfn[x] = low[x] = ++tim;
	int ver;
	for(int i = head[x]; i; i = nex[i]) {
		ver = to[i];
		if(!dfn[ver]) {
			tarjan(ver, i);
			low[x] = min(low[x], low[ver]);
			if(low[ver] > dfn[x]) {
				vis[i] = vis[i ^ 1] = 1;
			}
		}
		else if(i != (f ^ 1)) low[x] = min(low[x], low[ver]);
	}
}

int find(int x) {
	return fa[x] == x ? x : fa[x] = find(fa[x]);
}

void dfs(int x) {
	bel[x] = cnt; int ver;
	for(int i = head[x]; i; i = nex[i]) {
		ver = to[i];
		if(bel[ver] || vis[i]) continue;
		dfs(ver);
	}
}

void bfs() {
	queue<int> q; 
	q.push(1), dep[1] = 1;
	int now, ver;
	while(!q.empty()) {
		now = q.front(), q.pop();
		for(int i = hb[now]; i; i = nb[i]) {
			ver = tb[i];
			if(dep[ver]) continue;
			dep[ver] = dep[now] + 1, f[ver][0] = now;
			for(int j = 1; j <= 17; j ++) f[ver][j] = f[f[ver][j - 1]][j - 1];
			q.push(ver); 
		}
	}
}

int lca(int x, int y) {
	if(dep[x] > dep[y]) swap(x, y);
	for(int i = 17; i >= 0; i --) {
		if(dep[f[y][i]] >= dep[x]) y = f[y][i];
	}
	if(x == y) return x;
	for(int i = 17; i >= 0; i --) {
		if(f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
	}
	return f[x][0];
}

void init() {
	tot = 1, tim = 0;
	memset(vis, 0, sizeof(vis));
	memset(head, 0, sizeof(head));
	memset(hb, 0, sizeof(hb));
	memset(f, 0, sizeof(f));
	memset(bel, 0, sizeof(bel));
	memset(dep, 0, sizeof(dep));
	memset(dfn, 0, sizeof(dfn));
	for(int i = 1; i <= n; i ++) fa[i] = i;
}

int main() {
	int u, v, l;
	while(~scanf("%d %d", &n, &m) && n && m) {
		init();
		for(int i = 1; i <= m; i ++) {
			scanf("%d %d", &u, &v);
			add(u, v), add(v, u);
		}
		for(int i = 1; i <= n; i ++) {
			if(!dfn[i]) tarjan(i);
		}
		cnt = 0;
		for(int i = 1; i <= n; i ++) {
			if(!bel[i]) ++cnt, dfs(i);
		}
		ans = cnt - 1, t = 1;
		for(int i = 2; i <= tot; i ++) {
			u = to[i ^ 1], v = to[i];
			if(bel[u] == bel[v]) continue;
			bdd(bel[u], bel[v]);
		}
		bfs();
		scanf("%d", &Q);
		printf("Case %d:\n", ++T);
		while(Q --) {
			scanf("%d %d", &u, &v);
			u = bel[u], v = bel[v];
			u = find(u), v = find(v);
			l = lca(u, v);
			while(dep[u] > dep[l]) {
				fa[u] = f[u][0], ans --;
				u = find(u);
			}
			while(dep[v] > dep[l]) {
				fa[v] = f[v][0], ans --;
				v = find(v);
			}
			printf("%d\n", ans);
		}
		puts("");
	} 
	return 0;
}

学校网络

题目传送门

一些学校连接在一个计算机网络上,学校之间存在软件支援协议,每个学校都有它应支援的学校名单(学校 A 支援学校 B,并不表示学校 B 一定要支援学校 A)。

当某校获得一个新软件时,无论是直接获得还是通过网络获得,该校都应立即将这个软件通过网络传送给它应支援的学校。

因此,一个新软件若想让所有学校都能使用,只需将其提供给一些学校即可。

现在请问最少需要将一个新软件直接提供给多少个学校,才能使软件能够通过网络被传送到所有学校?

最少需要添加几条新的支援关系,使得将一个新软件提供给任何一个学校,其他所有学校就都可以通过网络获得该软件?

缩点板题,缩完了之后,问题一的答案为没有入度的点数,问题二答案为没有入度和没有出度点数的最大值。

点击查看代码
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define ll long long
const int M = 1e4 + 5, N = 5e6 + 5;

int n, m, tot, ans, ans2, head[M], to[N << 1], nex[N << 1];  
void add(int x, int y) {
	to[++tot] = y, nex[tot] = head[x], head[x] = tot;
}

int tim, cnt, top, in[M], out[M], sta[M], dfn[M], bel[M], low[M];
void tarjan(int x) {
	low[x] = dfn[x] = ++tim, sta[++top] = x;
	int ver, ch = 0;
	for(int i = head[x]; i; i = nex[i]) {
		ver = to[i];
		if(!dfn[ver]) {
			tarjan(ver);
			low[x] = min(low[x], low[ver]);
		}
		else if(!bel[ver]) low[x] = min(low[x], dfn[ver]);
	}
	if(low[x] == dfn[x]) {
		bel[x] = ++cnt;
		while(sta[top] != x) {
			bel[sta[top]] = cnt;
			top --;
		}
		top --;
	}
}
int main() {
	scanf("%d",&n);
	int u, v;
	for(int i = 1; i <= n; i ++) {
		scanf("%d", &u);
		while(u) add(i, u), scanf("%d",&u);
	}
	for(int i = 1; i <= n; i ++) {
		if(!dfn[i]) tarjan(i);
	}
	for(int i = 1; i <= n; i ++) {
		for(int j = head[i]; j; j = nex[j]) {
			v = to[j];
			if(bel[i] != bel[v]) {
				in[bel[v]] ++, out[bel[i]] ++;
			} 
		}
	}
	for(int i = 1; i <= cnt; i ++) {
		if(!in[i]) ans ++;
		if(!out[i]) ans2 ++;
	} 
	if(cnt == 1) printf("1\n0");
	else printf("%d\n%d\n", ans, max(ans, ans2));
	return 0;
} 

总结

\(Tarjan\) 真的妙,妙不可言。

但凡我有 \(tarjan\) 百分之一的脑子,我也不至于被网课折磨得死去活不来(又开始魔怔了)

posted @ 2022-07-13 10:28  Spring-Araki  阅读(49)  评论(0编辑  收藏  举报