有向图的强连通分量(SCC)与无向图割点的一些细节,Tarjan

前言

Tarjan是美国计算机学家Robert Tarjan提出的一系列基于dfs的图论算法,可以解决很多连通性问题。
这里主要说关于有向图的强连通分量(SCC)与无向图割点算法的一些细节,而不是介绍算法本身。

有向图的强连通分量(SCC)

模板题
在这里插入图片描述
先在这里贴出题解
还有不明白的可以看董老师的视频
再不明白再看董老师的另一个视频

上代码:

#include<iostream>
#include<vector>
#include<stack>
#include<algorithm>
using namespace std;
vector<vector<int>> a,b;
stack<int> s;
int n,m;
int h1[10005],h2[10005];
int ring[10005];
bool vis[10005];
int low[10005];
int dfn[10005],cnt;
int cntx=1;
int dfs1(int u) {
	if(dfn[u]) return dfn[u];
	s.push(u);
	vis[u]=true;
	low[u]=dfn[u]=++cnt;
	for(auto&v:a[u])
		if(!dfn[v]||vis[v])
			low[u]=min(low[u],dfs1(v));
	if(dfn[u]==low[u]) {
		while(!s.empty()) {
			int v=s.top();
			s.pop();
			vis[v]=false;
			ring[v]=cntx;
			h2[cntx]+=h1[v];
			if(v==u) break;
		}
		cntx++;
	}
	return low[u];
}
void Tarjan() {
	for(int i=1; i<=n; i++)
		dfs1(i);
	for(int i=1;i<=n;i++)
		for(auto&j:a[i])
			if(ring[i]!=ring[j])
				b[ring[i]].push_back(ring[j]); 
}
int f[10005];
int dfs2(int u) {
	if(vis[u]) return f[u];
	vis[u]=true;
	for(auto&v:b[u])
		f[u]=max(f[u],dfs2(v));
	f[u]+=h2[u];
	return f[u];
}
int main() {
	cin>>n>>m;
	for(int i=0; i<=n; i++) a.push_back({}),b.push_back({});
	for(int i=1; i<=n; i++) cin>>h1[i];
	for(int i=1; i<=m; i++) {
		int u,v;
		cin>>u>>v;
		a[u].push_back(v);
	}
	Tarjan();
//	for(int i=1;i<=n;i++)
//		cout<<i<<' '<<ring[i]<<' '<<h2[i]<<endl;
	for(int i=1; i<=n; i++)
		dfs2(i);
	cout<<*max_element(f+1,f+1+n);
	return 0;
}

主要的问题:

  1. 注意Tarjan函数里,建立新图b的部分,枚举起点,邻接表找终点,复杂度是O(n),如果提前初始化,枚举起点和终点,复杂度是O(n2)。
  2. 注意dfs1函数里,仅当 dfn[u]==low[u] 时,表示u是一个强连通分量的根,因为u没有再向上连边了。
  3. 然后就是当满足此条件时,stack里面的元素并不全是在一个强连通分量上的,而是仅从栈顶到u符合条件,所以只说!s.empty()是不够的,追加一个条件,要求if(u==v)break;就好了。
  4. 更新low数组的条件:节点v没有被访问过,或者仍在栈中。这是因为如果不在栈中,那u->v就是横叉边。
  5. 还有一个关于low数组问题,等一下说

无向图割点

模板题
在这里插入图片描述
你谷题解区
还有不明白的看董老师的视频

然后是代码:

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
vector<vector<int>> a;
int n,m;
int dfn[20005],low[20005],cnt;
vector<int> ans;
int dfs(int u,int fa,bool f) {
	if(dfn[u]) return dfn[u];
	low[u]=dfn[u]=++cnt;
	int k=0;
	for(auto&v:a[u]) 
		if(v^fa) {
			if(!dfn[v]) k++;
			bool flag=!dfn[v];
			low[u]=min(low[u],dfs(v,u,false));
			if(!f&&low[v]>=dfn[u]&&flag)
				ans.push_back(u);
		}
	if(f&&k>1) 
		ans.push_back(u);
	return low[u];
}
void Tarjan() {
	for(int i=1;i<=n;i++) 
		dfs(i,0,true);
}
int main() {
	cin>>n>>m;
	for(int i=0;i<=n;i++) a.push_back({});
	for(int i=1;i<=m;i++) {
		int u,v;
		cin>>u>>v;
		a[u].push_back(v);
		a[v].push_back(u);
	}
	Tarjan();
	sort(ans.begin(),ans.end());
	ans.erase(unique(ans.begin(),ans.end()),ans.end());
	cout<<ans.size()<<endl;
	for(auto&i:ans)
		cout<<i<<' ';
	return 0;
}

主要的问题:

  1. 注意不能走原来的边回到父亲节点,所以限制v^fa(异或是不等于)
  2. 注意dfs函数里,仅有!dfn[v]时,才有k++,否则不是新的子树。
  3. 注意必须满足三个条件,在for循环里才能统计ans:第一个条件是不能是根节点;第二个条件是low[v]>=dfn[u];第三个条件是节点v必须是第一次被访问,这是由于,如果不是第一次被访问,那么判断条件就不再是low[v]>=dfn[u],而是dfn[v]>=dfn[u],而由于v不是第一次被访问,所以v的时间戳一定比u要早,一定不满足条件。
  4. 提一个小点,就是在以root为根进行搜索的情况下,不存在横叉边,只存在后向边,因为如果存在一条横叉边,那么就意味着访问到了之前的一颗子树,既然访问到了之前的一颗子树,那么访问之前那一颗子树的时候为什么没有通过横叉边(在那时,横叉边就变成了树边)访问到现在的这颗子树呢?这不符合逻辑。
  5. 然后:

关于tarjan算法,一直有一个很大的争议,就是low[u]=min(low[u],dfn[v]);

这句话,如果改成low[u]=min(low[u],low[v])就会wa掉,但是在求强连通分量时却没有问题

具体区别参考这篇文章
至于为什么求强连通分量的时候没有问题呢?因为从点u能追溯到点v,就已经代表了u、v构成强连通图了,那么如果v的追溯值显示v还与点x构成强连通图,那么u自然也与x构成强连通图,因此追溯值被改变也没有影响了。

后记

于是皆大欢喜

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值