连通性相关
连通性相关
在有向图中, 的定义一般指点 能到达的最小时间戳。在无向图中, 的定义一般指点 不经过它与父亲的树枝边,至多走一条非树枝边,能到达的最小时间戳。
强连通分量
强连通的定义是:有向图 强连通是指, 中任意两个结点连通。
强连通分量(Strongly Connected Components,SCC)的定义是:极大的强连通子图。——摘自 OI Wiki
Tarjan
用 Tarjan 求强连通分量(缩点),缩点后有向图变为一个 DAG。
算法简介
定义
如果有向图 的每两个顶点都强连通,称 是一个强连通图。有向非强连通图的极大强连通子图,称为强连通分量。
四条边
树枝边: dfs 搜索树上的边。
前向边:与 dfs 方向一致,从某个结点指向其某个子孙的边。
后向边:与 dfs 方向相反,从某个结点指向其某个祖先的边。(返祖边)
横叉边:从某个结点指向搜索树中的另一子树中的某结点的边。
流程
Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。
搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
定义 为节点 搜索的次序编号(时间戳), 为 或 的子树能够追溯到的最早的栈中节点的次序号。
由定义可以得出, 。 为树枝边, 为 的父节点 。
- 节点 可以到达 ,节点 为父亲,所以节点 也可以到达 。
。 为指向栈中节点的后向边/横叉边。
- 因为此时 还在栈中,所以 一定是 的祖先。
当结点 搜索结束后,若 时,则以 为根的搜索子树上所有还在栈中的节点是一个强连通分量( pop 一直到 就行)。
- 当 时:
若子树里的点还在,则 一定等于 。子树的所有点可以到达 , 可以到达子树所有点。
SCC-Tarjan模板
void tarjan(int u) { dfn[u]=low[u]=++dfn0; st[++top]=u; for(int v : to[u]) { if(!dfn[v]) { tarjan(v); low[u]=min(low[u],low[v]); }else if(!num[v]) { low[u]=min(low[u],low[v]);//与 dfn[v] 等价 } } if(low[u]==dfn[u]) { ++cnt; while(st[top+1]!=u) scc[cnt].push_back(st[top]), num[st[top]]=cnt, --top; } }
模版题2:受欢迎的牛
例题
code
#include<bits/stdc++.h> #define ll long long #define pf printf #define sf scanf using namespace std; const int N=1e5+7,mod=1e9+7; int n,m; int u,v; vector<int> son[N]; int num; int c[N]; int dfn[N],low[N],scc[N]; int cnt,sum; int val[N]; ll ans,ans2=1; int st[N],top; void Tarjan(int u){ dfn[u]=low[u]=++num; st[++top]=u; for(int i=0;i<son[u].size();i++){ int v=son[u][i]; if(!dfn[v]){ Tarjan(v); low[u]=min(low[u],low[v]); }else if(!scc[v]){ low[u]=min(low[u],dfn[v]); } } if(low[u]==dfn[u]){ scc[u]=++cnt; val[cnt]=c[u]; sum=1; while(st[top]!=u){ scc[st[top]]=cnt; if(c[st[top]]<val[cnt]) sum=1; else if(c[st[top]]==val[cnt]) sum++; val[cnt]=min(val[cnt],c[st[top]]); top--; } top--; ans+=val[cnt]; ans2=ans2*sum%mod; } } int main(){ cin>>n; for(int i=1;i<=n;i++){ cin>>c[i]; } cin>>m; for(int i=1;i<=m;i++){ cin>>u>>v; son[u].push_back(v); } for(int i=1;i<=n;i++) if(!dfn[i]) Tarjan(i); pf("%lld %lld\n",ans,ans2); }
Kosaraju
求一个无向图的强连通分量的方法是枚举每个点 i,如果还没有访问过点 i,就 dfs(i)
,然后把 dfs 过程中的点缩到一个 SCC。
借鉴无向图的方法,可以发现在有向图上这种方法仍然正确当且仅当我们按照(假设已经缩完点的)DAG 的拓扑序反序 dfs。否则一个强连通分量将搜到另一个强连通分量,然后它们两回合在一起,显然不对。
如何找到 dfs 的正确顺序呢?我们以 1 开始进行 dfs,每个节点出栈时 push 到 st 数组(其实是栈)中,按照 st 的倒序求 SCC 就是正确的。
因为假设强连通分量 u 可以到达强连通分量 v,那么 v 会先进入 st,求 SCC 时按照倒序就会先求 v,这样求 u 时就不会搜到 v 了。
求 SCC 的方法和无向图一样。
总结:
- dfs(1),st 记录结点出栈顺序。
- 按 st 的倒序 dfs,可以搜到的即为一个 SCC。
双连通分量
在一张连通的无向图中,对于两个点 和 ,如果无论删去哪条边(只能删去一条)都不能使它们不连通,我们就说 和 边双连通。
在一张连通的无向图中,对于两个点 和 ,如果无论删去哪个点(只能删去一个,且不能删 和 自己)都不能使它们不连通,我们就说 和 点双连通。
边双连通具有传递性,即,若 边双连通, 边双连通,则 边双连通。
点双连通不具有传递性,反例如下图, 点双连通, 点双连通,而 不点双连通。
边双连通分量
在一张连通的无向图中,对于两个点 和 ,如果无论删去哪条边(只能删去一条)都不能使它们不连通,我们就说 和 边双连通。
边双连通具有传递性,即,若 边双连通, 边双连通,则 边双连通。
两个点是边双连通的,当且仅当它们的图上路径中不包含桥。(如果没遍历过并且连的边不是割边就标记为同一块边双)
边双连通分量就是极大边双连通块。
无向图边双缩点后成为一棵树,所有树边是桥。
求出所有割边即可。
可以用 Tarjan。
注意模板题有重边,因此 的定义是不经过上一次走过的边,走至多一条非树枝边可以到达的最小时间戳。
void tarjan(int u,int la) { dfn[u]=low[u]=++dfn0; st[++top]=u; for(auto i : to[u]) { int v=i.se; if(!dfn[v]) { tarjan(v,i.fi); low[u]=min(low[u],low[v]); }else if(i.fi!=la) { low[u]=min(low[u],dfn[v]); } } if(low[u]==dfn[u]) { ++cnt; num[u]=cnt, vec[cnt].push_back(u); while(st[top]!=u) num[st[top]]=cnt, vec[cnt].push_back(st[top]), --top; --top; } }
点双连通分量
一个点可以属于多个点双。
Tarjan 求点双。
RT,黑色边为树边,红色边为返祖边。(由于是无向图,因此没有横叉边)
设 为 的时间戳, 表示点 不经过父亲可以到达的最小时间戳。
若 是 的儿子,,则 属于同一个点双, 为该点双时间戳最小的节点,退栈加入该点双直到退掉 ,将 加入点双但是不退栈 。
割点和桥
割点和桥一般针对无向图,因此没有横叉边。
桥
对于一个无向图,如果删掉一条边后图中的连通分量数增加了,则称这条边为桥或者割边。
RT, 即为该图唯一的桥。
计算桥(割边)的方法:
首先,容易知道割边一定是 DFS 树的树边。记录 DFS 树上指向 的边,它是割边当且仅当以 为根的子树内没有向其它子树或祖先连边。
如果 的后代只能连回 自己。即 ,则 是桥。
code
代码不保证正确
void tarjan(int rt,int u,int f) { dfn[u]=low[u]=++cnt; for(int v : to[u]) { if(!dfn[v]) { tarjan(rt,v,u), low[u]=min(low[u],low[v]); if(low[v]>dfn[u]) ans.push_back({u,v}); }else if(v!=f) low[u]=min(low[u],dfn[v]); } }
割点
对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点(又称割顶)。
RT, 即为该图唯一的割点。
Tarjan 求割点。
表示点 经过至多一条非树边可以到达的最小时间戳。
计算割点的方法:
- 对于 DFS 树的树根,它是割点当且仅当它有两个及以上的子树。
- 对于其它任意一个点,当且仅当以它为根的子树内没有向其它子树或祖先连边。因此在 dfs 过程中,如果一个点 存在一个子节点 ,使得 的后代只能连回 。即 ,则 是割点。
Code
割点和割边代码唯一的区别就是 和 。以及割点需要多判一个根。
void tarjan(int rt,int u,int f) { dfn[u]=low[u]=++cnt; int son=0; for(int v : to[u]) { if(!dfn[v]) { ++son, tarjan(rt,v,u), low[u]=min(low[u],low[v]); if(low[v]>=dfn[u] && u!=rt && !isans[u]) isans[u]=1, ans.push_back(u); }else if(v!=f) low[u]=min(low[u],dfn[v]); } if(son>=2 && u==rt) isans[u]=1, ans.push_back(u); }
圆方树
圆方树可以用来解决将无向图按点双缩点,但是原来的点的信息仍要保留的问题。(不像强连通分量缩点有的可以直接删除圆点,仅保留强连通分量编号)
将一个无向图变为一棵树:
把每个点双建一个方点,将点双中所有点建一个圆点,与该方点相连。
RT.
圆方树中,每条链一定是由圆点、方点交错形成。
建好圆方树后,依题意在树上求解即可。
Code
点双改一点即可。(代码为外向树,建双向边关掉注释即可)
void Tarjan (int u) { dfn[u]=low[u]=++cnt; st[++top]=u; for(int i=head[u];i;i=e[i].ne) { int v=e[i].to; if(!dfn[v]) { Tarjan(v); low[u]=min(low[u],low[v]); if(dfn[u]==low[v]) { tot++; to[u].push_back(tot); // to[tot].push_back(u); while(st[top]!=v){ to[tot].push_back(st[top]); // to[st[top]].push_back(tot); top--; } to[tot].push_back(v); // to[v].push_back(tot); top--; } }else{ low[u]=min(low[u],dfn[v]); } } }
经验
圆方树、点双。
本文来自博客园,作者:wing_heart,转载请注明原文链接:https://www.cnblogs.com/wingheart/p/18357734
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】