【图论基础】 tarjan算法


割点与桥

算法简介

如果无向图中删除某个点$u$,连通块数量增加,则$u$为图的一个割点;本算法用来找到图中所有割点;如果删除无向图的一条边 $e$,连通块数量增加,则称 $e$ 为桥。

算法思想

利用时间戳概念在图中建立一个DFS树,用$dfn$数组存储每个点的时间戳,用$low$数组存储每个点能到达的最远的祖先,如果存在一个儿子节点$v$使得$low[v] \geq low[u]$,则$u$为割点。总时间复杂度为:$O(n)$。如果 $low[v]> dfn[u]$,则连接 u, v 的边为桥。

注意:dfs的第一个节点,如果只有一个儿子则不是割点,否则一定是一个割点。

强连通分量

算法简介

该算法主要处理有向图的强连通分量问题。对于有向图中的一个强连通分量 (Strongly Connected Component,一个集合中的点相互可达) 可以看做一个点,这样有向图就构成一个DAG,更便于处理问题。

算法思想

对一个图进行dfs,形成一棵dfs树,用dfn表示该点的时间戳,用low存储该点在dfs树中能到达的最远的祖先的时间戳,找到每个scc的第一个点。把每个点访问的点入栈,找到scc的第一个点时将栈中该点后的所有点倒出形成一个集合。

scc的第一个点满足一个性质:$low[i]=dfn[i]$。利用这个性质可以轻松找到该点,在dfs要时刻记得更新low值。对于i,如果连接的点未遍历过,则遍历该点,回溯时用儿子节点更新i的low:$low[i]=min(low[son(i)],low[i])$。如果连接的点遍历过并且还未形成集合:$low[i]=min(low[i],dfn[u])$。整个过程的时间复杂度为$O(N)$。

注意:如果连接的点已经成环,则无法利用该点更新low值!

dfs完毕后,枚举每个点并枚举旧图的边并连接新图的边,注意打一些标记,不要连成自环、重边。时间复杂度为$O(M)$。

总时间复杂度为线性。

代码

#include<stack>
int n,cnt,scc_cnt,dfn[2020],low[2020],scc[2020];
bool vis[2020];
//dfn表示时间戳,low表示能到达的最小祖先,scc表示新集合的编号
//用结构体表示两个图
struct MAP{
	int e,be[2020],to[maxx],ne[maxx],d[maxx];
	MAP(){e=0;}
	void init(){
		For(i,0,e) to[i]=ne[i]=0;
		For(i,0,n) be[i]=d[i]=0;
		e=0;
	}
	void add(int x,int y){
		to[++e]=y;
		ne[e]=be[x];
		be[x]=e;
		d[y]++;
	}
}New,Old;

//初始化
void init(){
	ans=cnt=scc_cnt=0;
	For(i,0,n){
		dfn[i]=low[i]=scc[i]=vis[i]=0;
		c[i]=oo;
	}
	Old.init(); New.init();
}
//tarjan
stack<int>s;
void tarjan(int id){
	s.push(id);
	dfn[id]=low[id]=++cnt;
	for(int i=Old.be[id];i;i=Old.ne[i]){
		int go=Old.to[i];
		if(!dfn[go]){
			tarjan(go);
			low[id]=min(low[id],low[go]);
		}
		else if(!scc[go]) low[id]=min(low[id],dfn[go]);
	}
	if(low[id]==dfn[id]){
		scc_cnt++;
		while(1){
			int x=s.top(); s.pop();
			scc[x]=scc_cnt;
			if(x==id) break;
		}
	}
}
//建新图
void build(){
	For(id,1,n){
		vis[scc[id]]=1;
		for(int i=Old.be[id];i;i=Old.ne[i]){
			int go=Old.to[i];
			if(!vis[scc[go]]){
				New.add(scc[id],scc[go]);
				vis[scc[go]]=1;
			}
		}
		for(int i=Old.be[id];i;i=Old.ne[i])	vis[Old.to[i]]=0;
		vis[scc[id]]=0;
	}
}
int main()
{
	init();
	For(i,1,n) if(!dfn[i]) tarjan(i);
	build();
	return 0;
}

双连通分量

推荐博客

算法简介

默认为无向图

点双连通分量:子图上任意两点存在至少两条不存在公共节点的路径,也可以说是不存在割点的子图

边双连通分量:子图上任意两点存在至少两条不存在公共边的路径,也可以说是不存咋割边的子图

点双连通分量与边双连通分量的异同:

同:两者都是一个环

异:可以用下图直接解释,整个图是一个边双连通分量,但不是点双连通分量,因为点3是一个割点。1,2,3是点双与边双,3,4,5也是点双与边双,但是合在一起1,2,3,4,5只是一个边双,即边双连通分量具有传递性,而点双连通分量不具有传递性。

算法思想

边双连通分量类似强分量算法类似处理即可。

点双连通分量:开一个栈将边压入,当一个点判定为割点时,将栈中所有边弹出,这些边连接成一个点双连通分量。(注意代码中,弹栈的位置)

代码

对一个连通图,输出割点,割边,点双连通个数,点双连通分量最多的边数

题目链接:http://121.48.165.90/contest/170/problem/J

#include<bits/stdc++.h>
#define LL long long
#define endl '\n'
using namespace std;
const int N = 2022;
int n, m, cnt, num, mx, cut_edge, cut_node, dfn[N], low[N], edge_num[N], sz[N];
vector<int> edge[N];
stack<pair<int, int>> st;
void dfs(int id, int fa){
	dfn[id] = low[id] = ++cnt;
	bool if_cut_node = 0;
	int son = 0;
	for(auto u : edge[id]){
		if(u == fa) continue;
		bool if_cut_edge = 0;
		if(!dfn[u]){
			++son;
			st.push(make_pair(id, u));
			dfs(u, id);
			low[id] = min(low[id], low[u]);
			if(low[u] >= dfn[id]){
				if_cut_node = 1;
				++num;
				int tmp = 0;
				while(1){
					pair<int, int> e = st.top(); st.pop();
					mx = max(mx, ++tmp);
					if(e.first == id && e.second == u) break;
				}
			}
			if(low[u] > dfn[id]) if_cut_edge = 1;
		}
		else if(dfn[u] < dfn[id]){
			st.push(make_pair(id, u));
			low[id] = min(low[id], dfn[u]);
		}
		if(if_cut_edge) ++cut_edge;
	}
	if(!fa) cut_node += (son > 1);
	else if(if_cut_node){ ++cut_node; }//cout << id << endl; }
}
int main(){
	ios::sync_with_stdio(false); cin.tie(0);
#ifndef ONLINE_JUDGE
	freopen("input.in", "r", stdin);
	freopen("output.out", "w", stdout);
#endif
	cin >> n >> m;
	for(int i = 1, u, v; i <= m; ++i){
		cin >> u >> v;
		edge[u].push_back(v);
		edge[v].push_back(u);
	}
	for(int i = 1; i <= n; ++i)
		if(!dfn[i]) dfs(i, 0);
 	cout << cut_node << " " << cut_edge << " " << num << " " << mx;
	return 0;
}

文章作者: Knowledge
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Knowledge !
  目录