割点
割点的定义为:对于一个无向图,如果删除了一个点后图的连通分量个数增加了,即为图不再连通了,那么该点为割点。如图,只有一个割点 2 2 2。
至于如何用 Tarjan 求。在 dfs 中打上时间戳,即访问节点的顺序。将数据储存在数组 dfn 中。
另外需要一个数组 low,用于存储每个节点,不经过其入边能到达的最小的时间戳。如 l o w 2 = 1 , l o w 5 = l o w 6 = 3 low_2 = 1, low_5= low_6 = 3 low2=1,low5=low6=3。特殊的,一个点的 l o w low low 是自己,当且仅当不通过它的入边时,它无法回到入边连的点,但以通过它下面的点回到自己,而不是原地不动。这意味着他与下面的点构成了一个极大的简单环。
对于某一个 x x x ,若存在一个 x x x 出边连向的点 y y y ,使得 l o w y ≥ d f n x low_y \ge dfn_x lowy≥dfnx ,即不能回到 x x x 祖先,那么 x x x 点为割点。值得一提的是,对于没有入边的点,若它的出边个数 ≥ 2 \ge 2 ≥2,那么也是割点。
更新 low
的伪代码如下:
如果 v 是 u 的出边 low[u] = min(low[u], low[v]);
否则
low[u] = min(low[u], dfn[v]);
#include<bits/stdc++.h>
const int N = 1e6 + 5;
using namespace std;
struct edge{
int to, nxt;
}e[N];
int head[N], tot;
void addedge(int x, int y){
e[++tot].to = y, e[tot].nxt = head[x], head[x] = tot;
}
int n, m;//n:点数 m:边数
int dfn[N], low[N], idx, res;
//dfn:记录每个点的时间戳 low:能不经过父亲到达最小的编号,idx:时间戳,res:答案数量
bool vis[N], cut[N];//cut:答案 vis:标记是否重复
void Tarjan(int x, int fa){//x当前点的编号,fa自己爸爸的编号
vis[x] = 1;//标记
low[x] = dfn[x] = ++idx;//打上时间戳
int child = 0;//每一个点儿子数量
for (int i = head[x]; i; i = e[i].nxt) {//访问这个点的所有邻居
int y = e[i].to;
if (!vis[y])
{
child++;//多了一个儿子
Tarjan(y,x);//继续
low[x] = min(low[x], low[y]);//更新能到的最小节点编号
if (fa != x && low[y] >= dfn[x] && !cut[x])//主要代码
//如果不是自己,且不通过父亲返回的最小点符合割点的要求,并且没有被标记过
//要求即为:删了父亲连不上去了,即为最多连到父亲
cut[x] = 1, res++;//记录答案
}
else if(y != fa) low[x] = min(low[x], dfn[y]);//如果这个点不是自己,更新能到的最小节点编号
}
if(fa == x && child >=2 && !cut[x])//主要代码,自己的话需要2个儿子才可以
cut[x] = 1, res++;//记录答案
}
int main(){
scanf("%d%d", &n, &m);//读入数据
for (int i = 1; i <= m; i++){//注意点是从1开始的
int x, y; scanf("%d%d", &x, &y);
addedge(x, y), addedge(y, x);
}//存图
for (int i = 1; i <= n; i++)//因为Tarjan图不一定联通
if (!vis[i]){
idx = 0;//时间戳初始为0
Tarjan(i, i);//从第i个点开始,父亲为自己
}
printf("%d\n", res);
for (int i = 1; i <= n; i++)
if (cut[i]) printf("%d%c", i, (i == n ? '\n' : ' '));//输出结果
return 0;
}
割边
割边的定义与割点类似,对于一个无向图,如果删掉一条边后图的连通分量数增加了,即为图不再连通了,那么该边为桥或者割边。如图,割边有 { 1 , 2 } , { 2 , 5 } \{1,2\} , \{2,5\} {1,2},{2,5}。
割边的求法也与割点类似, 对于某一个 x x x ,若存在一个点 y y y ,让 l o w x > d f n y low_x > dfn_y lowx>dfny 。可以发现,割点是满足 y y y 不经过 x x x 不能回到 x x x 的祖先,而 y y y 是不经过边不能回到 x x x,所以只有一个等于的不同。
点双联通分量
点双联通图的定义为:一个无向图中没有割点的极大联通子图。如图,点双联通分量有 { 1 , 2 } , { 2 , 5 } , { 2 , 3 , 4 } \{1,2\},\{2,5\},\{2,3,4\} {1,2},{2,5},{2,3,4}。
可以发现,如果一个子图是点双联通,那么图的顶点全在一个简单环中,如上图 { 2 , 3 , 4 } \{2,3,4 \} {2,3,4} 是一个简单环,是一个点双联通子图。特殊地,不超过两个点的子图也一定点双联通。
扩展到点双联通分量,唯一的区别是联通分量是极大的。如果一个节点 x x x 能回到它的祖宗 y y y,那么它们间的点构成了一个点双联通子图,但是不一定是点双联通分量。比如还有一个 y y y 的祖宗 z z z 且 x x x 也有连向 z z z 的边,那么构成了一个更大的点双联通子图。所以,如果一个点是一个点双联通分量中时间戳最小的点,那么当且仅当 子孙有连该点的边,而没有连该点祖先的边。
具体的。开一个栈,如果一个点是第一次访问,那么将它压入栈中。若当前点是割点,那么将栈中从该点到该点入边指向的点间所有的点,即为一个简单环,加入点双联通分量。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
struct edge{
int to, nxt;
}e[N];
int head[N], tot;
void addedge(int x, int y){
e[++tot].to = y, e[tot].nxt = head[x], head[x] = tot;
}
int n, m, dfn, cnt, dfn[N], low[N], cut[N];
vector <int> G[N], ans[N];
stack <int> st;
void Tarjan(int x, int fa) {
dfn[x] = low[x] = ++dfn; st.push(x);
int child = 0 ;
for (int i = head[x]; i; i = e[i].nxt) {
int y = e[i].to;
if(!dfn[y]) {
child++; Tarjan(y, x); low[x] = min(low[y] , low[x]);
if(low[y] >= dfn[x]) {
cut[x] = 1; ans[++cnt].push_back(x);
while(x!=st.top()) ans[cnt].push_back(st.top()), st.pop();
}
}else if (dfn[x] > dfn[y] && y != fa) low[x] = min(dfn[y], low[x]);
}
if (fa == 0 && child == 1) cut[x] = 0;
if (fa == 0 && child == 0) G[++cnt].push_back(x);
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++){
int x, y; scanf("%d%d", &x, &y);
addedge(x, y), addedge(y, x);
}
for (int i = 1; i <= n; i++) if(!dfn[i]) Tarjan(i, 0);
printf("%d\n", cnt);
for (int i = 1; i <= cnt; i++)
for (int j = 0; j < ans[i].size(); j++)
printf("%d%c", ans[i][j], (j == ans[i].size() - 1 ? '\n' :' '));
return 0;
}
边双联通分量
边双联通分量的定义为:一个无向图中没有割边的极大联通子图。如图,边双联通分量是且仅是全图。
至于求法。先一次 Tarjan 求出所有的割边,把这些割边删掉,即为在求联通分量的时候不访问,然后统计所有的联通分量即为边双联通分量。可以发现,一条边最多存在于一个边双中,而一个点可以存在于多个点双中。所以可以删割边,不能删割点。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int n, m;
int dfn[N], low[N], idx, cnt;
bool vis[N], cut[N];
struct edge{
int to, nxt, z; // 新增一个元素记录边的编号
}e[N];
int head[N], tot;
void addedge(int x, int y, int z){
e[++tot].to = y, e[tot].nxt = head[x], e[tot].z = z, head[x] = tot;
}
void Tarjan(int x, int fa){
vis[x] = true; low[x] = dfn[x] = ++idx;
int child = 0;
for (int i = head[x]; i; i = e[i].nxt) {
int y = e[i].to, z = e[i].z;
if (!vis[y]){
child++; Tarjan(y,x);
low[x] = min(low[x], low[y]);
if (low[y] > dfn[x] && !cut[x]) cut[z] = true;
}else if(y != fa) low[x] = min(low[x], dfn[y]);
}
}
vector <int> ans;
void dfs(int x){//求图中的联通分量
for (int i = head[x]; i; i = e[i].nxt) {
int y = e[i].to, z = e[i].z;
if (vis[y] || cut[z]) continue; //不重复不是割点
vis[y] = 1; ans.push_back(y);
dfs(y);
}
}
int main(){
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++){
int x, y; scanf("%d%d", &x, &y);
addedge(x, y, ++cnt), addedge(y, x, cnt);
}
for (int i = 1; i <= n; i++)
if (!vis[i]) idx = 0, Tarjan(i, i);
memset(vis, 0, sizeof(vis));
for (int i = 1; i <= n; i++)
if (!vis[i]){
vis[i] = 1;
ans.clear(); ans.push_back(i);
dfs(i);
for (int i = 0; i < ans.size(); i++) printf("%d ", ans[i]);
puts("");
}
return 0;
}
强连通分量
强连通分量的定义为:一个有向图中,任意两个点都能互相到达的极大联通子图。如图,强联通分量有 { 1 , 2 , 3 } , { 4 } , { 5 } \{1,2,3\},\{4\},\{5\} {1,2,3},{4},{5}。
与点双联通分量相似,如果一个子图是强联通,那么图的顶点全在一个简单环中。简单环是由回祖路构成的。值得一提的是,只有一个点的子图一定是强连通子图。扩展到强连通分量,还满足 子孙有连该点边,而没有连该点祖先的边。
具体的求法与点双联通分量略有不同。因为有向图中谈割点没有意义,所以通过判断一个点不通过它的入边,能不能回到它的祖先。如果能,即为 d f n x = l o w x dfn_x = low_x dfnx=lowx,这意味着不存在一条路径,使得 x x x 下面的点能回到 x x x 的祖先,而能回到自己。所以 x x x 及其下面的点构成了一个强联通分量。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
struct edge{
int to, nxt;
}e[N];
int head[N], tot;
void addedge(int x, int y){
e[++tot].to = y, e[tot].nxt = head[x], head[x] = tot;
}
int n, m, idx, cnt, dfn[N], low[N], cut[N], res, vis[N], color[N];
vector <int> G[N], ans[N];
stack <int> st;
void Tarjan(int x) {
dfn[x] = low[x] = ++idx; st.push(x); vis[x] = 1;
int child = 0 ;
for (int i = head[x]; i; i = e[i].nxt) {
int y = e[i].to;
if(!dfn[y]) {
child++;
Tarjan(y); low[x] = min(low[y] , low[x]);
}else if(vis[y]) low[x] = min(dfn[y], low[x]);
}
if (dfn[x] == low[x]){
int y; cnt++;
do{
ans[cnt].push_back(y = st.top()); vis[y] = 0; color[y] = cnt;
st.pop();
} while (x != y);
}
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++){
int x, y; scanf("%d%d", &x, &y);
addedge(x, y);
}
for (int i = 1; i <= n; i++) if(!dfn[i]) Tarjan(i);
printf("%d\n", cnt);
return 0;
}
缩点
在有向图中,将每个强连通分量缩成超级点,超级点通常具有所在强连通分量所有点的有用信息,且如果两个点之间有边,那么它们所在的超级点也有边。缩点后的图是一个 DAG,有向无环图。
在无向图中,将每个点双联通分量或边双联通分量缩成超级点。超级点通常具有所在强连通分量所有点的有用信息,且如果两个点之间右边,那么它们所在的超级点也有边。缩点后的图是一棵树。