【知识点复习】Tarjan 算法 与 图的连通性
前言
不知道为什么,以前特别喜欢 \(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\) 百分之一的脑子,我也不至于被网课折磨得死去活不来(又开始魔怔了)