前言
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;
}
主要的问题:
- 注意Tarjan函数里,建立新图b的部分,枚举起点,邻接表找终点,复杂度是O(n),如果提前初始化,枚举起点和终点,复杂度是O(n2)。
- 注意dfs1函数里,仅当 dfn[u]==low[u] 时,表示u是一个强连通分量的根,因为u没有再向上连边了。
- 然后就是当满足此条件时,stack里面的元素并不全是在一个强连通分量上的,而是仅从栈顶到u符合条件,所以只说!s.empty()是不够的,追加一个条件,要求if(u==v)break;就好了。
- 更新low数组的条件:节点v没有被访问过,或者仍在栈中。这是因为如果不在栈中,那u->v就是横叉边。
- 还有一个关于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;
}
主要的问题:
- 注意不能走原来的边回到父亲节点,所以限制v^fa(异或是不等于)
- 注意dfs函数里,仅有!dfn[v]时,才有k++,否则不是新的子树。
- 注意必须满足三个条件,在for循环里才能统计ans:第一个条件是不能是根节点;第二个条件是low[v]>=dfn[u];第三个条件是节点v必须是第一次被访问,这是由于,如果不是第一次被访问,那么判断条件就不再是low[v]>=dfn[u],而是dfn[v]>=dfn[u],而由于v不是第一次被访问,所以v的时间戳一定比u要早,一定不满足条件。
- 提一个小点,就是在以root为根进行搜索的情况下,不存在横叉边,只存在后向边,因为如果存在一条横叉边,那么就意味着访问到了之前的一颗子树,既然访问到了之前的一颗子树,那么访问之前那一颗子树的时候为什么没有通过横叉边(在那时,横叉边就变成了树边)访问到现在的这颗子树呢?这不符合逻辑。
- 然后:
关于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构成强连通图,因此追溯值被改变也没有影响了。
后记
于是皆大欢喜