强连通分量之tarjan缩点——杨子曰算法
这次是真的没有什么模板题,直接开讲吧!
首先什么是强连通分量呢?
有向图强连通分量:在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。
哦!我的天哪,我居然看懂了度娘的定义(内心有点小激动),相信机智的大佬们也看懂了,我就不多解释了,那我们就举个栗子(不是错别字)吧,Look at the 图:
每个圈里就是一个强连通分量
那我们可以在强连通分量上做什么算法呢?——缩点,将一个强连通分量变成一个点
为啥我们可以将一个强连通分量变成一个点因为强连通分量中的点是可以互相到达的,在某些情况下,这些点是等价的
为啥我们要把他们缩成一个点因为缩完点后的图没有环,是一张有向无环图,这种图有很多性质,能解决一些题目
OK,废话不多说,我们马上开始缩点:
缩点有3个算法:Kosaraju算法,Gabow算法和Tarjan算法,今天曰一个:Tarjan算法(哦,又是他,大佬膜拜膜拜——他发明了大量算法:一遍DFS求出LCA,一遍DFS求出所有割点,证明并查集合并的复杂度是O(α(n))(不要问我这是啥)
上面说了他还发明了一遍DFS求出所有割点的算法,这个算法与我们今天讲的缩点有密切联系,想法非常接近(←毕竟是同一个大佬搞出来的)(不知道的童鞋戳我)
首先你要先知道一个东西叫DFS树,比方说上面那个图:
它从5开始DFS的DFS树就长成这样:
DFS树,实线表示的是真实的DFS顺序,虚线是虽然没有DFS到,但它们两点间确实有边(地球人都能明白)
首先问题个问题,虚线边会不会连接两颗子树(横叉边),Of course not!
如果有这条虚线边,就说明原图中这两点见右边,如果有变那么通过这个点就可以直接DFS的另一个点,那么这两个点就会成父子关系,So,杨子曰:DFS树中没有横插边,也就是说虚线只会连接到这个节点的祖先
那么这时候,我们就可以用low[i]表示以i为根结点的这棵子树中的结点中虚线边连到最高节点,这个最高结点的DFS序(用dfn数组存下)
那么更新low[k](k下一个DFS结点是v)分两种情况
1.k是v的祖先(也就是遍历到k时,v还没有被遍历过)
low[k]=min(low[k],low[v]);
2.v是k的祖先(也就是遍历到k时,v已经被遍历过了)
low[k]=min(low[k],dfn[v])
(↑都是从我割点那篇文章复制过来的↑)
现在,我们再来仔细观察一下原图和DFS树,机智的你一定发现了什么玄机——每一个从下向上的虚线边,围成了一个强连通分量,也就是说在DFS的过程中如果有一个点i在DFS完它的子树后low[i]=dfn[i]就说明它到虚线边那一端所连成的链是一个强连通分量(已经被标记了的除外),然后标记一下就完了
在代码实现的时候要注意几个问题:
- 首先最开始DFS的点只选一个的话,不一定能走完整张图,所以要拿个循环扫一遍,如果这个点没有被标记就DFS
- 可以用一个bl数组表示缩完点后这个点属于哪个强连通分量
- 我们在找到强连通分量标记点时,可以用一个栈实现(这里的栈模拟系统栈),DFS到这个点时push进去,找到low[k]=dfn[k]的时候从栈顶一直pop到k这段就是一个强连通分量(这里讲不清楚,自己模拟一下应该比较好理解)
代码走起:
//scc统计当前强连通分量的个数
void dfs(int x){
dfn[x]=low[x]=++cnt;
s.push(x);
for (int i=head[x];i!=-1;i=edge[i].next){
int u=edge[i].to;
if (!dfn[u]){
dfs(u);
low[x]=min(low[x],low[u]);
}
else if (!bl[u]) low[x]=min(low[x],dfn[u]);
}
if (dfn[x]==low[x]){
scc++;
for (;;){
sz[scc]++;
int k=s.top();
s.pop();
bl[k]=scc;
if (k==x) break;
}
}
}
OK,完事
实在找不到缩点的模板,搞个NOIP提高组信息传递凑个数:
#include<bits/stdc++.h>
using namespace std;
const int maxm=200005,maxn=200005;
struct Edge{
int next,to;
}edge[maxm*2];
int n,m,nedge=0,cnt=0,scc=0;
int head[maxn],low[maxn],dfn[maxn],sz[maxn],bl[maxn];
stack<int> s;
void addedge(int a,int b){
edge[nedge].to=b;
edge[nedge].next=head[a];
head[a]=nedge++;
}
void dfs(int x){
dfn[x]=low[x]=++cnt;
s.push(x);
for (int i=head[x];i!=-1;i=edge[i].next){
int u=edge[i].to;
if (!dfn[u]){
dfs(u);
low[x]=min(low[x],low[u]);
}
else if (!bl[u]) low[x]=min(low[x],dfn[u]);
}
if (dfn[x]==low[x]){
scc++;
for (;;){
sz[scc]++;
int k=s.top();
s.pop();
bl[k]=scc;
if (k==x) break;
}
}
}
int main(){
memset(head,-1,sizeof(head));
int n;
scanf("%d",&n);
for (int i=1;i<=n;i++){
int x;
scanf("%d",&x);
addedge(i,x);
}
for (int i=1;i<=n;i++){
if (!dfn[i]) dfs(i);
}
int ans=200000000;
for (int i=1;i<=scc;i++){
if (sz[i]==1) continue;
ans=min(ans,sz[i]);
}
cout<<ans;
return 0;
}
未经作者允许,严禁转载:https://blog.csdn.net/HenryYang2018/article/details/81112467