Tarjan 割点割边 点双边双 强连通缩点

割点

割点的定义为:对于一个无向图,如果删除了一个点后图的连通分量个数增加了,即为图不再连通了,那么该点为割点。如图,只有一个割点 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 lowydfnx ,即不能回到 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,有向无环图

在无向图中,将每个点双联通分量或边双联通分量缩成超级点。超级点通常具有所在强连通分量所有点的有用信息,且如果两个点之间右边,那么它们所在的超级点也有边。缩点后的图是一棵

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值