强连通分量-缩点
初学只需要背代码就好了,而复习的时候要考虑的就多了(
(打牛客的时候用到,发现忘得差不多了)
概念解读
- 连通:在无向图中,从任意点
A
都可以到达任意点B
。 - 强连通:在有向图中,从任意点
A
都可以到达任意点B
。 - 弱连通:将有向图看作无向图后,从任意点
A
都可以到达任意点B
。
特殊地,单独的点也可以看作强连通分量。
说白了,强连通分量就是环。
而用于求强连通分量的算法,就是大名鼎鼎的 tarjan!
而强连通分量最大的应用,则是求出环之后进行缩点。
模板
首先分析模板为什么能用缩点来做
这不显然吗题目就叫缩点
由于每个点的点权只计算一次,所以一个点重复走对于答案的贡献没有影响。
那么,如果走到了环上的一点,则走完环上的所有点一定会是最有答案的一部分。
所以就可以找到所有的强连通分量,然后把强连通分量缩成点,然后构造新图。
而构造出的新图,绝对是一个 DAG
(有向无环图)!
然后就可以用拓扑去解决这个 DAG
。
tarjan
那么,求强连通分量的 tarjan 到底是怎样实现的呢。
众所周知,对树进行 dfs
的时候,有一个名为时间戳的神奇东西,他可以记录 dfs
时的顺序。
而对图进行 dfs
时,则会出现一下几个概念:
- 树边:属于
dfs
生成树的边。 - 非树边:不属于生成树的边,分为:
- 返祖边:在
dfs
时指向祖先节点的边。 - 前向边:在
dfs
时指向子树中节点的边。 - 横叉边:在
dfs
时指向已经访问过的点,但不是祖先节点。
- 返祖边:在
而我们求强连通分量,实际上就是找环。
在 dfs
中,我们用栈来储存目前搜到但没有构成强连通分量的。
tarjan 除了时间戳,还引入了另一个变量:回溯值 low
。
\(low_u\):在 \(u\) 的子树中能够回溯到的最早的已经在栈中的结点。
显然,环的组成是部分树边和一条返祖边,前向边和横叉边则不会成环。
同样在 dfs
时,只有树边和返祖边会影响 low
。
low
的初始值均为当前节点的时间戳。
-
分析树边:
当前节点向下发出的树边连接的即为子节点。首先继续
dfs
,到回溯时通过与子节点的low
值取较小值更新当前节点的low
。此时当前节点的low
满足定义。 -
分析回溯边:
当前节点若发出回溯边,则必定指向自己的祖先,根据
low
的定义,当前节点的low
即可根据回溯的祖先节点的时间戳更新。
而当分析完所有发出的边后,若时间戳与 low
相等,则说明子树中没有节点能到达更早的在栈中的节点,当前节点即可作为强连通分量的根,将栈中的节点弹出,作为一个强连通分量储存。
代码:
void Tarjan(int now)
{
in[now]=1;h.push(now);
dfn[now]=low[now]=++dfns;
for(int i=fir[now];i;i=nex[i])
{
int p=poi[i];
if(!dfn[p])
{//没有 dfs 过,属于树边
Tarjan(p);
low[now]=min(low[now],low[p]);
}
else if(in[p])//dfs 过,但是还在栈中,属于回溯边
low[now]=min(low[now],dfn[p]);
}
if(dfn[now]==low[now])
{//强连通分量的根,弹出
int ls;ks++;
do{
ls=h.top();h.pop();in[ls]=0;
bel[ls]=ks;sum[ks]+=a[ls];
}while(ls!=now);
}
}
找完强连通分量之后,便要重建图,即缩点。
我们遍历所有的边,若边的两端不在同一强连通分量,则说明重建图中有这个边。
为不与原图冲突,重建图采用 vector
存图(原图采用邻接表存图)。
for(int i=1;i<=m;i++)
{
int ru=bel[u[i]],rv=bel[v[i]];
if(ru==rv)continue;
e[ru].push_back(rv);
deg[rv]++;
}
由于缩点之后的图绝对是 DAG
,所以直接通过拓扑解决后续问题。
for(int i=1;i<=ks;i++)
if(!deg[i])q.push(i),val[i]=sum[i];
while(q.size())
{
int now=q.front();q.pop();
int len=e[now].size();
for(int i=0;i<len;i++)
{
int p=e[now][i];deg[p]--;
val[p]=max(val[p],val[now]+sum[p]);
if(deg[p]==0)q.push(p);
}
}
完整代码
const int inf=1e5+7;
int n,m,ans,a[inf];
int u[inf],v[inf];
int fir[inf],nex[inf],poi[inf],cnt;
void ins(int x,int y)
{
nex[++cnt]=fir[x];
poi[cnt]=y;
fir[x]=cnt;
}
int dfn[inf],low[inf],dfns;
bool in[inf];
stack<int>h;
int ks,bel[inf],sum[inf];
void Tarjan(int now)
{
in[now]=1;h.push(now);
dfn[now]=low[now]=++dfns;
for(int i=fir[now];i;i=nex[i])
{
int p=poi[i];
if(!dfn[p])
{
Tarjan(p);
low[now]=min(low[now],low[p]);
}
else if(in[p])
low[now]=min(low[now],dfn[p]);
}
if(dfn[now]==low[now])
{
int ls;ks++;
do{
ls=h.top();h.pop();in[ls]=0;
bel[ls]=ks;sum[ks]+=a[ls];
}while(ls!=now);
}
}
vector<int>e[inf];
int deg[inf],val[inf];
queue<int>q;
int main()
{
n=re();m=re();
for(int i=1;i<=n;i++)
a[i]=re();
for(int i=1;i<=m;i++)
{
u[i]=re(),v[i]=re();
ins(u[i],v[i]);
}
for(int i=1;i<=n;i++)
if(!dfn[i])Tarjan(i);
for(int i=1;i<=m;i++)
{
int ru=bel[u[i]],rv=bel[v[i]];
if(ru==rv)continue;
e[ru].push_back(rv);
deg[rv]++;
}
for(int i=1;i<=ks;i++)
if(!deg[i])q.push(i),val[i]=sum[i];
while(q.size())
{
int now=q.front();q.pop();
int len=e[now].size();
for(int i=0;i<len;i++)
{
int p=e[now][i];deg[p]--;
val[p]=max(val[p],val[now]+sum[p]);
if(deg[p]==0)q.push(p);
}
}
for(int i=1;i<=ks;i++)
ans=max(ans,val[i]);
wr(ans,'\n');
return 0;
}