Tarjan

1|0有向图

1|1搜索树

对有向图进行 dfs 时,递归经过的有向边形成的树,被称为搜索树。

树边:边 (x,y) 在搜索树中。

前向边:搜索树中存在一条 xy 的路径。

后向边:搜索树中存在一条 yx 的路径。

横叉边:搜索树中不存在 xy 的路径和 yx 的路径,且 dfny<dfnx

1|2强连通分量

强连通图:在有向图中,对于每一对 x,y,xy, 都存在从 xy 的路径和从 yx 的路径,则该有向图为强连通图。

强连通分量:有向图中的极大强连通子图。

可以用 Tarjan 算法 O(n+m) 来求有向图的强连通分量。

若点 x 为其所在的强连通分量在搜索树中第一个搜索到的点,称其为该强连通分量的根。

dfnx:点 xdfs 序,即 dfs 中第一次搜索到点 x 的次序。

lowx:在以点 x 为根的子树内的点通过非树边的一条边到达的 dfn 最小的节点 ydfn 值,y 能到达 x

st:栈内元素为当前已访问过但没有判定为在某个强连通分量的点。

visx,表示点 x 是否在栈中,用来判定一条边是否为返祖边。

当点 x 满足 dfnx=lowx 时,其为其所在的强连通分量的根。

当枚举点 x 的出边 (x,y) 时,若 y 为通过树边到达,则用 lowy 更新 lowx,若 y 为通过非树边到达,则用 dfny 更新 lowx

void tarjan(int x) { dfn[x]=low[x]=++dfn_cnt,st[++top]=x,vis[x]=true; for(int i=head[x];i;i=e[i].nxt) { int y=e[i].to; if(!dfn[y]) tarjan(y),low[x]=min(low[x],low[y]); else if(vis[y]) low[x]=min(low[x],dfn[y]); } if(low[x]==dfn[x]) { col_cnt++; int now; do now=st[top--],vis[now]=false,col[now]=col_cnt; while(now!=x); } } ...... for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i);

2|0无向图

2|1点双连通分量

割点:无向连通图中,如果将其中一个点以及所有连接该点的边去掉,图分裂为两个或两个以上的子图,则称该点为割点。

点双连通图:没有割点的无向连通图。

点连通分量:无向图中的极大点双连通子图。

选定一个根节点,从该根节点开始用 dfs 遍历整个图。

对于根节点,若其有两棵即以上的子树,则根节点为割点。因为如果去掉这个点,这两棵子树就不能互相到达。

对于非根节点 x,存在边 (x,y),如果 dfnxlowy,则 x 为割点。因为从 y 到以 x 为根的子树之外的点都必须经过点 x

void tarjan(int x,int root) { int son=0; dfn[x]=low[x]=++dfn_cnt; for(int i=head[x];i;i=e[i].nxt) { int y=e[i].to; if(!dfn[y]) { tarjan(y,root),low[x]=min(low[x],low[y]); if(x!=root&&dfn[x]<=low[y]) cut[x]=true; if(x==root) son++; } else low[x]=min(low[x],dfn[y]); } if(son>1) cut[x]=true; } ...... for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i,i);

求割点时维护一个栈即可求出每个点双连通分量。

x 满足为割点时,就依次弹栈,直到弹出 y 就停止,x 还需留在栈中,因为割点 x 可能属于多个点双连通分量。

void tarjan(int x) { dfn[x]=low[x]=++dfn_cnt,st[++top]=x; for(int i=head[x];i;i=e[i].nxt) { int y=e[i].to; if(!dfn[y]) { tarjan(y),low[x]=min(low[x],low[y]); if(dfn[x]<=low[y]) { col_cnt++; int now; do now=st[top--],col[now]=col_cnt; while(now!=y); } } else low[x]=min(low[x],dfn[y]); } }

2|2边双连通分量

桥:无向连通图中,如果一条边去掉,图分裂为两个子图,则称该边为桥。

边双连通图:没有桥的无向连通图。

边连通分量:无向图中的极大边双连通子图。

对于点 x,存在边 (x,y),如果 dfnx<lowy,则边 (x,y) 为桥。因为以 y 为根的子树内的点都必须经过边 (x,y)

记录入边 link 的作用是不处理儿子到父亲边,对于边 (x,y),若处理了儿子到父亲边,lowy 就会等于 dfnx,将无法判定桥,记录具体是哪一条入边是为了处理重边的情况。

求出桥后即可求出边双连通分量。

void tarjan(int x,int link) { dfn[x]=low[x]=++dfn_cnt; for(int i=head[x];i;i=e[i].nxt) { int y=e[i].to; if(!dfn[y]) { tarjan(y,i),low[x]=min(low[x],low[y]); if(dfn[x]<low[y]) bri[i]=bri[i^1]=true; } else if(i!=(link^1)) low[x]=min(low[x],dfn[y]); } } void dfs_col(int x) { col[x]=co_cnt; for(int i=head[x];i;i=e[i].nxt) { int y=e[i].to; if(col[y]||bri[i]) continue; dfs_col(y); } } ...... for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i,0); for(int i=1;i<=n;++i) if(!col[i]) col_cnt++,dfs_col(i);

给定一个无向连通图,至少添加多少条边可以把它变为边双连通图。

先对边双连通分量进行缩点,得到一棵无根树,设该树度数为 1 的节点个数为 cnt,则将该树变为边双连通图至少添加 cnt+12 条边。

在叶子节点带权下求出树的带权重心,然后将不同子树内的节点两两配对连边即可,因为一个子树内的叶子节点个数不超过 cnt2,因此一定能匹配完后覆盖所有边。


__EOF__

本文作者lhm_
本文链接https://www.cnblogs.com/lhm-/p/12229463.html
关于博主:sjzez 的一名 OI 学生
版权声明:转载标明出处
声援博主:希望得到宝贵的建议
posted @   lhm_liu  阅读(597)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示