tarjan算法——割点、桥,强连通分量、缩点

概念:

割点:

在一张无向连通图中,若将结点u去除后,该图不再连通,则称结点u为割点

桥:

在一张无向连通图中,若将边m去除后,该图不再连通,则称边m为桥

强连通分量:

若一张有向图的子图G'中的任意两个结点可相互到达,则称该子图G'为强连通分量

缩点:

在一张有向图中,若存在强连通分量,那么在某些题目条件下,该强连通分量可看作一个新的结点代替原来的多个点,(当然要重新分配它的点权,入边以及出边),从而将一张有向图变为一张有向无环图,方便我们进行下一步的遍历

tarjan算法:

1.dfs树:

对于一个无向连通图G=(V,E),选取一个结点R作为根,对其进行一次dfs遍历,所经过的点与边构成一棵树G'=(V,E'),那么,我们遍历所经过的边E'叫做树边,E-E'叫做非树边

由于这是一张无向图,那么对于每条非树边而言,其两端所连接的结点一定是一个祖先和一个子孙,我们又把这种边叫做返祖边。注意:在有向图中,存在连接没有祖先子孙关系结点的边,叫做横叉边

2.dfn与low数组及其更新方法:

我们可以定义两个数组:

dfn:指一个结点被第一次访问的时间戳

low:low[u]指从u出发,经过任意多条树边,最多经过一条非树边,可以回到的最早的祖先的时间戳

因此,这两个数组的计算方法为:

dfn的计算方法:

初始化t=1

每访问一个结点u,结点u的dfn就等于t,然后t+=1

low的计算方法:

每访问一个结点v,我们就将其low的值更新为其dfn的值

 对于父结点u而言,我们只能够通过子结点v来更新u的low值

因此,对于子结点v而言:

若子结点v未被访问过,这代表连接u和v的边为树边,则先dfs访问v,访问完后,用low[v]的值来更新low[u]

若子结点v已经被访问过了,这代表连接u和v的边为返祖边,则用dfn[v]的值来更新low[u]

ps:为什么这里不用low[v]的值来更新low[u]呢?这里是为了保证v只能经过一条非树边来更新low值,倘若在用low[u]的值更新low[v]的值之前,low[u]已经通过一条返祖边用其祖先low[u']的值更新了,那么low[v]的值也会更新为low[u'],这里会对后面割点的判断产生影响

割点的判断:

题目链接:https://www.luogu.com.cn/problem/P3388

设父结点为u,子结点为v,那么我们利用dfn[u]与low[v]的值,就可以判断出u是否为割点了

PS:这里给出的图不一定连通,我们只需对每一个无向连通图都做一遍判断割点就可以了

对于u为非根结点的时候:

这里给出三种情况的讨论:

1.dfn[u]<low[v],这意味着v无法回溯到比u更早的时间戳,也就是说,当u被除去时,v会与u的祖先失去联系,使得图不再连通,那么这个时候,u为割点

2.dfn[u]=low[v],这意味着v最多可以回溯到u,当u被除去时,v同样会和u的祖先失去联系,图不再连通,u为割点

3.dfn[u]>low[v],这意味着v可以回溯到比u更早的祖先,即使u被除去了,图也依然连通

这三种方法的判断根本就取决于上面提到的low数组的更新方式:

如果v可以通过u将low[v]的值更新到low[u']的话,那也就是说low[v]<dfn[u],u不是割点。显然,这种判断是错误的。

因为,这是建立在v以u为跳板的基础上才回溯到u'的,也就是说,如果将u去除,low[v]将不再小于dnf[u]。所以我们规定,low为v至多经过一条非树边所回溯到的最早的时间戳,也就保证了不存在跳板结点的可能。

当u为根结点的时候:

这个时候,显然对任意的v都存在low[v]>=dfn[u],那么所有的根结点都是割点吗?当然不是。所以这个时候,我们就需要给出另外的判断方法;

1.当根节点存在两个及以上的儿子时,根节点是割点

2.除此之外,根节点不是割点

这个结论是显然的,不过要注意,上面所涉及的父结点的子节点一说,一定要是由父结点u亲自用dfs访问的结点v,结点v才是u的子结点,否则不是。

实现代码如下:

#include <iostream>
using namespace std;
const int N = 10010, M = 200050;
int to[M], nxt[M], h[N], tot;
int dfn[N], low[N], t;
int son, root, cnt;
bool cut[N];
int n, m;
void add(int a, int b) {
	to[++tot] = b;
	nxt[tot] = h[a];
	h[a] = tot;
}
int min(int a, int b) {
	return a < b ? a : b;
}
void tarjan(int u) {
	dfn[u] = low[u] = t++;
	for (int i = h[u], v; v = to[i]; i = nxt[i]) {
		if (dfn[v] == 0) {
			if (u == root) son++;
			tarjan(v);
			low[u] = min(low[u], low[v]);
			if (u != root && low[v] >= dfn[u]) {
				if (!cut[u]) cnt++;
				cut[u] = true;
			}
		}
		else {
			low[u] = min(low[u], dfn[v]);
		}
	}
}
void start(int u) {
	son = 0; root = u;
	tarjan(u);
	if (son > 1) {
		cnt++; cut[u] = true;
	}
}
int main() {
	cin >> n >> m;
	for (int i = 1,a,b; i <= m; i++) {
		cin >> a >> b;
		add(a, b); add(b, a);
	}
	t = 1;
	for (int i = 1; i <= n; i++) {
		if (dfn[i] == 0) start(i);
	}
	cout << cnt << endl;
	for (int i = 1; i <= n; i++) {
		if (cut[i]) cout << i << ' ';
	}
	return 0;
}

缩点:

题目链接:https://www.luogu.com.cn/problem/P3388

 有了tarjan算法,我们就可以利用dfn与low的值在有向图中找出强连通分量,从而进行缩点

具体实现思路为:

我们每访问一个结点,就将其放入栈内,显然从栈顶到栈底,结点的dfn值递减

若一个父结点u在遍历完所有的子结点v之后,其low的值仍未被更新,即dfn[u]==low[u],那么它以及它后面进栈(且仍在栈内)的结点就构成一个强连通分量

为什么在它后面进栈且仍在栈内的结点会与它构成一个强连通分量呢?

由于这些结点还在栈内,说明它们的low值都被更新过,也就是说,他们都能回到一个更早的结点u,从而形成了一个环,那么在我们回退到u之前,它们都不会出栈,因此它们形成一个强连通分量

找到强连通分量之后,我们就可以进行缩点操作,建一张新图,且这张图一定为有向无环图,从而进行更方便的遍历操作

实现代码如下:

#include <iostream>
#include <stack>
using namespace std;
const int N = 10050, M = 150050;
class Edge {
public:
	int to, nxt;
}; Edge e[2][M]; int tot[2];
int a[N], h[2][N];
int color[N], dfn[N], low[N], t, co;
bool book[M], vis[N], svis[N];
int dp[N], p[N];
stack<int> s;
int n, m;
void add(int a, int b,int i) {
	e[i][++tot[i]].to = b;
	e[i][tot[i]].nxt = h[i][a];
	h[i][a] = tot[i];
}
int min(int a, int b) {
	return a < b ? a : b;
}
int ma(int a, int b) {
	return a > b ? a : b;
}
void tarjan(int u) {
	dfn[u] = low[u] = t++;
	for (int i = h[0][u], v; v = e[0][i].to; i = e[0][i].nxt) {
		if (dfn[v] == 0) {
			s.push(v); svis[v] = true;
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}
		else if(svis[v]) {//要该结点在栈内才更新low值,否则不更新
			low[u] = min(low[u], dfn[v]);
		}
	}
	if (dfn[u] == low[u]) {//染色分新点
		int v;
		while (s.top() != u) {
			v = s.top(); s.pop();
			svis[v] = false;
			color[v] = co;
			p[co] += a[v];
		}
		v = s.top(); s.pop();
		svis[v] = false;
		p[co] += a[v];
		color[v] = co++;
	}
}
void build(int u) {//建新图
	for (int i = h[0][u], v; v = e[0][i].to; i = e[0][i].nxt) {
		if (!book[i]) {
			book[i] = true;
			if (color[u] != color[v]) {
				add(color[u], color[v], 1);
			}
			build(v);
		}
	}
}
void dfs(int u) {//由于是有向无环图,可以用dp求答案
	if (dp[u] == 0) dp[u] = p[u];
	for (int i = h[1][u], v; v = e[1][i].to; i = e[1][i].nxt) {
		if (!vis[v]) {
			vis[v] = true;
			dfs(v);
		}
		dp[u] = max(dp[u], dp[v] + p[u]);
	}
}
int main() {
	cin >> n >> m;
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
	}
	for (int i = 1, a, b; i <= m; i++) {
		cin >> a >> b;
		add(a, b, 0);
	}
	for (int i = 1; i <= n; i++) {
		add(n + 1, i, 0);//建立超级源点1 防止存在多个入度为0的点
	}
	t = co = 1;
	s.push(n + 1); svis[n+1] = true;
	tarjan(n + 1);
	build(n + 1);
	for (int i = 1; i < co; i++) {
		add(co, i, 1);//同理,建立超级源点2
	}
	vis[co] = true;
	dfs(co);
	cout << dp[co];
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值