强连通分量-缩点
初学只需要背代码就好了,而复习的时候要考虑的就多了(
(打牛客的时候用到,发现忘得差不多了)
概念解读
- 连通:在无向图中,从任意点
A
都可以到达任意点B
。 - 强连通:在有向图中,从任意点
A
都可以到达任意点B
。 - 弱连通:将有向图看作无向图后,从任意点
A
都可以到达任意点B
。
特殊地,单独的点也可以看作强连通分量。
说白了,强连通分量就是环。
而用于求强连通分量的算法,就是大名鼎鼎的 tarjan!
而强连通分量最大的应用,则是求出环之后进行缩点。
模板
首先分析模板为什么能用缩点来做
这不显然吗题目就叫缩点
由于每个点的点权只计算一次,所以一个点重复走对于答案的贡献没有影响。
那么,如果走到了环上的一点,则走完环上的所有点一定会是最有答案的一部分。
所以就可以找到所有的强连通分量,然后把强连通分量缩成点,然后构造新图。
而构造出的新图,绝对是一个 DAG
(有向无环图)!
然后就可以用拓扑去解决这个 DAG
。
tarjan
那么,求强连通分量的 tarjan 到底是怎样实现的呢。
众所周知,对树进行 dfs
的时候,有一个名为时间戳的神奇东西,他可以记录 dfs
时的顺序。
而对图进行 dfs
时,则会出现一下几个概念:
- 树边:属于
dfs
生成树的边。 - 非树边:不属于生成树的边,分为:
- 返祖边:在
dfs
时指向祖先节点的边。 - 前向边:在
dfs
时指向子树中节点的边。 - 横叉边:在
dfs
时指向已经访问过的点,但不是祖先节点。
- 返祖边:在
而我们求强连通分量,实际上就是找环。
在 dfs
中,我们用栈来储存目前搜到但没有构成强连通分量的。
tarjan 除了时间戳,还引入了另一个变量:回溯值 low
。
:在 的子树中能够回溯到的最早的已经在栈中的结点。
显然,环的组成是部分树边和一条返祖边,前向边和横叉边则不会成环。
同样在 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; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】