【学习笔记】图论连通性
啊啊啊啊啊!
先引用一篇犇的:)))
缩点
弱连通:
对于有向图中两点 \(x\) 和 \(y\),它们在所有边为无向时存在一个环使它们相连。
强连通:
对于有向图中两点 \(x\) 和 \(y\),存在一个环使它们相连。
强连通子图:
对于有向图 \(G = (V, E)\),如果对于一个 \(V\) 的子集 \(V_0\) 满足,\({\forall x, y \in V_0}\),\(x\) 和 \(y\) 均满足强连通,则称 \(V_0\) 为一个强连通子图。
强连通分量(SCC):
极大的强连通子图。(一个图中可以有多个)
- 极大的的含义:对于一个强连通子图 \(V_0\),满足 \(\forall V_0 \subset V_1\),且 \(V_1\) 都不是强连通子图。
Tarjan 算法
一些定义:
-
在 dfs 过程中遍历的边称为树边,未遍历的边称为非树边。
-
\(dfn_x\) 表示图中 \(x\) 号节点在 dfs 算法中是第 \(x\) 个遍历到的。
-
\(low_x\) 表示 \(x\) 号节点经过若干条树边后再经过至多一条满足条件的非树边能到达的 \(dfn\) 最小的点的值。
实现方法:
-
算出 \(dfn_x\) 并初始化 \(low_x\):可以全局记录一个时间戳 \(T\),并使
dfn[x] = low[x] = ++T
。 -
枚举 \(x\) 的所有出边指向的点 \(y\)。有两种情况:
-
\(y\) 还没有被遍历过。可以直接调用
dfs(y)
,因为该边是树边,所以直接更新low[x] = min(low[x], low[y])
。 -
\(y\) 已经被遍历过了。如果 \(y\) 和 \(x\) 在同一个 SCC(\(y\) 存在到 \(x\) 的路径),则更新
low[x] = min(low[x], dfn[y])
。2.1. 如何知道是否在一个 SCC 里呢?(判断横叉边)
我们可以考虑开一个栈,在进入
dfs(x)
是将 \(x\) 入栈,并在找到 \(x\) 所在的 SCC 的所有点后将 \(x\) 弹出栈。所以,此时如果 \(y\) 在这个栈中,则证明 \(y\) 到 \(x\) 存在路径,否则不存在。因此如果 \(y\) 此时在栈中则更新
low[x] = min(low[x], dfn[y])
。否则不用更新。
- 结束 dfs 时,判断 \(x\) 是否为其所在 SCC 内 \(dfn\) 最小的点。
-
如果 \(dfn_x = low_x\),则 \(x\) 是 \(dfn\) 最小的点。此时将栈中 \(x\) 及以上的元素弹出。这些元素即是跟 \(x\) 处于同一 SCC 的点。
-
否则 \(x\) 不是 \(dfn\) 最小的点,直接结束 dfs。
代码如下:
vector<int> g[N];
vector<int> scc[N];
int w[N], dfn[N], low[N], T, cnt;
int st[N], top;
bool ins[N];void tarjan(int u){dfn[u] = low[u] = ++T;st[++top] = u, ins[u] = 1;for(int v : g[u]){if(!dfn[v]){tarjan(v);low[u] = min(low[u], low[v]);} else if(ins[v]){low[u] = min(low[u], dfn[v]);}}if(dfn[u] == low[u]){cnt++;while(st[top] != u){scc[cnt].push_back(st[top]);ins[st[top--]] = 0;}scc[cnt].push_back(st[top]);ins[st[top--]] = 0;}
}
一个 SCC 实际上就对应着一个可以单点经过多次的非简单环,那么如果我们把 SCC 缩成一个点,也就意味着把一个一般有向图变成了 DAG。
由于 Tarjan 算法以及后续的缩点只需要遍历一次图,所以算法的总时间复杂度为 \(O(n+m)\)。
然后 DAG 上可以用拓扑排序 DP 来解决问题。
trick:统计出来的 SCC 里 \(cnt\) 由大到小就是拓扑序。
P3387 【模板】缩点
板子。注意 trick 运用方式。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e4+5;int n, m;
vector<int> g[N];
vector<int> scc[N]; // scc 里 cnt 由大到小就是拓扑序
vector<int> gn[N]; // scc 新图
int w[N], dis[N], dfn[N], low[N], T, cnt, ans;
int st[N], top;
bool ins[N];
int inscc[N], dp[N];void tarjan(int u){dfn[u] = low[u] = ++T;st[++top] = u, ins[u] = 1;for(int v : g[u]){if(!dfn[v]){tarjan(v);low[u] = min(low[u], low[v]);} else if(ins[v]){low[u] = min(low[u], dfn[v]);}}if(dfn[u] == low[u]){cnt++;while(st[top] != u){scc[cnt].push_back(st[top]);inscc[st[top]] = cnt;dis[cnt] += w[st[top]];ins[st[top--]] = 0;}scc[cnt].push_back(st[top]);inscc[st[top]] = cnt;dis[cnt] += w[st[top]];ins[st[top--]] = 0;}
}int main(){ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);cin>>n>>m;for(int i=1; i<=n; i++)cin>>w[i];for(int i=1; i<=m; i++){int u, v; cin>>u>>v;g[u].push_back(v);}for(int i=1; i<=n; i++)if(!dfn[i]) tarjan(i);for(int i=1; i<=n; i++){for(int j : g[i]){if(inscc[i] != inscc[j])gn[inscc[i]].push_back(inscc[j]);}}// dpfor(int i=cnt; i>=1; i--){ dp[i] = max(dp[i], dis[i]);for(int j : gn[i]){dp[j] = max(dp[j], dp[i] + dis[j]);}ans = max(dp[i], ans);}cout<<ans;return 0;
}
割点与割边
割点:
将某点从图中去掉后,得到了一个非连通图,则称这个点是割点。
割边:
将某边从图中去掉后,得到了一个非连通图,则称这条边是割边。
dfs 树:
对于一个无向图,通过 dfs 算法得到的一颗生成树。
- 在 dfs 过程中遍历的边称为树边,未遍历的边称为环边。
- 所有的环边一定是返祖边。
- 没有横叉边。
一些定义:
-
\(dfn_x\) 表示图中 \(x\) 号节点在 dfs 算法中是第 \(x\) 个遍历到的。
-
\(low_x\) 表示 \(x\) dfs 树的子树内,通过环边能回到的 \(dfn\) 最小的点。
实现方法(如何判断一个点 \(x\) 是割点):
-
若 \(x\) 是 dfs 树的根,\(x\) 是割点当且仅当 \(x\) 有不少于两个儿子。
-
否则,\(x\) 是割点当且仅当存在一个 \(x\) dfs 树的儿子 \(y\) 使得 \(low_y \ge dfn_x\)。
证明:若删去 \(x\),则 \(y\) 子树内没有向 \(x\) 祖先以及其它子树连接的边,故 \(y\) 子树与其它点会形成两个连通块。故 \(x\) 是割点。
实现方法(如何判断边 \((x, y)\) 是割边):
-
若 \((x, y)\) 是环边,则该边一定不是割边,因为删除该边之后生成树不受影响。
-
若 \((x, y)\) 是树边,假设 \(x\) 是 \(y\) 的父亲,则该边是割边当且仅当 \(low_y > dfn_x\)。
证明:若 \(low_y > dfn_x\),则删除该边后,\(y\) 子树内与 \(y\) 子树外会分成两个连通块,故该边为割边。否则,在删除边后 \(y\) 子树内与 \(y\) 子树外仍然连通,故该边不是割边。
点双与边双
割点决定点双,割边决定边双。
点双连通图:
如果一个无向图不存在割点,则称该图是一个点双连通图。
等价定义:图中任意两点都存在两条除了起点终点外点不相交的路径。
边双连通图:
如果一个无向图不存在割边,则称该图是一个边双连通图。
等价定义:图中任意两点都存在两条边不相交的路径。
点双连通分量:
极大的点双连通子图。
边双连通分量:
极大的边双连通子图。
思路:
边双连通分量缩点后得到的图为树,而点双连通分量“缩点”后得到的图为圆方树。
圆方树的含义是用圆点表示原图上的点,方点(新建点)表示不同的点双。(再把原点双的边去掉就得到了一棵树)
P8436 【模板】边双连通分量
板子。注意重边处理方式。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 5e5+5;int n, m;
vector<int> g[N];
vector<int> bs[N];
int dfn[N], low[N], T;
int st[N], top, cnt;
bool vis[N]; // 是否被遍历过void tarjan(int u, int fa, int id){dfn[u] = low[u] = ++T;st[++top] = u; vis[u] = 1;for(int v : g[u]){if(v == fa && id == 0){id = 1;continue;// 处理重边}if(!vis[v]){tarjan(v, u, 0);low[u] = min(low[u], low[v]);} else{low[u] = min(low[u], dfn[v]);}}if(dfn[u] == low[u]){cnt++;while(st[top] != u){bs[cnt].push_back(st[top]);top--;}bs[cnt].push_back(st[top]);top--;}
}int main(){ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);cin>>n>>m;for(int i=1; i<=m; i++){int u, v; cin>>u>>v;g[u].push_back(v);g[v].push_back(u);}for(int i=1; i<=n; i++)if(!vis[i]) tarjan(i, 0, 0);cout<<cnt<<"\n";for(int i=1; i<=cnt; i++){cout<<bs[i].size()<<" ";for(int j : bs[i]) cout<<j<<" ";cout<<"\n";}return 0;
}
P8435 【模板】点双连通分量
板子。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 5e5+5;int n, m;
vector<int> g[N];
vector<int> ds[N];
int dfn[N], low[N], T;
int st[N], top, cnt;
bool vis[N]; // 是否被遍历过void tarjan(int u, int fa){dfn[u] = low[u] = ++T;st[++top] = u; vis[u] = 1;int son = 0;for(int v : g[u]){if(v == fa) continue;if(!vis[v]){son++;tarjan(v, u);low[u] = min(low[u], low[v]);if(low[v] >= dfn[u]){cnt++;while(st[top+1] != v){ds[cnt].push_back(st[top]);top--;}ds[cnt].push_back(u);}} else{low[u] = min(low[u], dfn[v]);}}if(fa == 0 && son == 0) ds[++cnt].push_back(u); // 孤立点判定
}int main(){ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);cin>>n>>m;for(int i=1; i<=m; i++){int u, v; cin>>u>>v;g[u].push_back(v);g[v].push_back(u);}for(int i=1; i<=n; i++)if(!vis[i]) tarjan(i, 0);cout<<cnt<<"\n";for(int i=1; i<=cnt; i++){cout<<ds[i].size()<<" ";for(int j : ds[i]) cout<<j<<" ";cout<<"\n";}return 0;
}
P2860 [USACO06JAN] Redundant Paths G
由题意很自然想到可以边双缩点,然后建新图并找到叶子节点的个数。
答案为 \(\lfloor \frac{s+1}{2} \rfloor\)(\(s\) 为叶子节点数)。
转证明。
口胡一下另一种:将度数为 2 的点缩了之后,每次连距离最长的两个叶子节点即可。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 5e3+5;int n, m;
vector<int> g[N];
vector<int> bs[N];
int dfn[N], low[N], inbs[N], T;
int st[N], top, cnt, leaf;
bool vis[N]; // 是否被遍历过
int ind[N];void tarjan(int u, int fa, int id){dfn[u] = low[u] = ++T;st[++top] = u; vis[u] = 1;for(int v : g[u]){if(v == fa && id == 0){id = 1;continue;// 处理重边}if(!vis[v]){tarjan(v, u, 0);low[u] = min(low[u], low[v]);} else{low[u] = min(low[u], dfn[v]);}}if(dfn[u] == low[u]){cnt++;while(st[top] != u){bs[cnt].push_back(st[top]);inbs[st[top--]] = cnt;}bs[cnt].push_back(st[top]);inbs[st[top--]] = cnt;}
}int main(){ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);cin>>n>>m;for(int i=1; i<=m; i++){int u, v; cin>>u>>v;g[u].push_back(v);g[v].push_back(u);}for(int i=1; i<=n; i++)if(!vis[i]) tarjan(i, 0, 0);for(int i=1; i<=n; i++){for(int j : g[i]){if(inbs[i] != inbs[j]){ind[inbs[j]]++;// 每个点都会被遍历到一次,因为之前已经连的是无向边,所以统计一个就行。}}}for(int i=1; i<=cnt; i++)if(ind[i] == 1) leaf++;cout<<(leaf+1)/2;return 0;
}