耳分解与双极定向 - 双联通性的新视角
之所以说耳分解与双极定向给出的是双联通性的新视角,是因为它们给出了双联通性的一些有力的等价性质,对于提升我们对双联通性的理解以及有关处理双联通性更复杂的问题提供了一个切入点。
以下边双联通图指的是任意不同两点间存在两条边不交简单路径,点双联通图指任意不同两点间存在两条点不交简单路径(也就是说两个点的图不算点双),所有图默认可以有重边,不能有自环。
耳分解
定义图 对于子图 的耳为一个顶点序列 满足任意相邻两点间有边,且 ,,且 必须互不相同。特别地,如果一个耳满足 则称为一个开耳。
耳分解讲的是将图分解成一个子图序列 ,满足 是一个环,,。且 必须是 的一个耳(去掉首尾的点)。特别地,如果这些耳都是开耳则称这是一个开耳分解。
我们可以说明:
-
一个无向图边双联通 该无向图存在耳分解
-
一个无向图点双联通 该无向图存在开耳分解
证明:
考虑对于加入耳的过程归纳,右推左都是平凡的。
我们考虑通过一个无向图的 dfs 生成树构造耳/开耳分解。
初始时我们随便选一个包含 dfs 生成树根的环作为 。
我们维持不变式:当前在耳分解中的所有子图都是包含 dfs 生成树根的一个树上连通块。
现在如果耳分解中最大的子图点集不是全集,那么一定能找到一个 满足其父亲在耳分解中而它不在。我们考虑找出 dfs 树上覆盖 的返祖边,对于双联通图,我们总能找到 满足 是 的祖先, 是 的后继。这样考虑 就是一个耳。特别地,对于点双联通图我们一定能找到 是 严格祖先的边,所以可以构造一个开耳。
将这个耳(开耳)加入耳分解中。直到当前最大子图的点集是全集那么就将所有未加入边集的边作为一个耳加入。
耳分解与强连通性的关系
我们考虑这样一个问题:给一张无向图的边定向,使其成为强连通图。
通过观察 dfs 树,我们发现这张图想要有解至少是一张边双联通图,否则 dfs 树上一定有没被覆盖的边,这条边如何定向都会使原图拥有至少两个强连通分量。
进一步发现,我们可以通过边双联通图的耳分解构造一组解:我们将每一个耳定向成有向路径,依次加入,容易发现始终保持强连通性。
事实上耳分解也可以轻松刻画强联通性。我们这里可以依然仿照无向图的定义给出有向图中耳的定义,即一条首尾可能相接的有向链。仿照无向图的构造,我们抽取 tarjan 算法中的外向生成树用无向图类似的方式可以给强连通图执行耳分解。
双极定向问题
再一次考虑一个新的定向问题:给一张无向图的边定向,使其成为 DAG。
这个问题显然太蠢了,我们研究的是它的加强形式“双极定向”。也就是说给定无向图中的两个节点 ,你需要定向成 DAG 使得 成为唯一的无入度点, 成为唯一的无出度点。
乍一看难以入手,但是我们能给出一个简单的结论:一张无向图中的 能双极定向当且仅当连接 后原图成为点双联通图。
这个神奇的结论如果直接用点双联通性入手难以证明,我们考虑点双联通的等价条件开耳分解。
首先说明充分性:
借助开耳分解的构造过程,我们容易使 成为一个包含 的环。
那么去掉 这条边,剩下的只有一条条首尾不交的开耳路径,我们只需要比较开耳的首尾是否有偏序关系,然后依着这个偏序关系的方向给这条路径定向加入图中。这样显然维持了定向后的图是一张 DAG 的不变式。
接下来必要性:
考虑反证,如果不是点双那么对于非平凡()的情况一定存在割点,显然 不能成为割点,删除割点后一定会形成一个包含 和一个不包含 的连通块。考虑这个割点与这两个连通块间的连边方式,发现要么它不能保证无环,要么不能保证双极性。
如何构造双极定向呢?一个 的构造就是直接拿出开耳分解暴力 dfs 判断两点偏序关系,但这并不是我们想要的。
我在网上发现了 zx2003 给出的一个简便的方法:原问题可以通过缩二度点来减小规模。容易发现如果一个图存在双极定向等价于缩完二度点的图有双极定向。
进一步地,拿出 dfs 树,只保留树边和每一个点子树中最浅的返祖边不会影响点双联通性,我们发现叶子节点永远是二度点,那么除了 之间的链将其它部分不断的剥叶子,可以缩减到只有一条直链的平凡情况。
另一种好理解的想法是我们直接按拓扑序遍历,对于一个耳,只需要看它的那一边被拓扑序优先遍历到了,那么直接顺着这个方向遍历这个耳就可以了。注意如果多个耳需要同时遍历,那么要先遍历深的耳。
ix35 在白鹭兰中给了一种很简单的实现。我们对于一个点如果其父亲或子树中最浅的返祖边指向的节点遍历到了那么就立刻遍历该节点。我们维护一个队列,按照剥叶子的顺序将除 链上的每个点加入其父亲和其最浅返祖边的队列中,这样就保证了“要先遍历深的耳”。然后从根节点开始遍历 链,每遍历到一个点就 dfs 按顺序它遍历队列中的所有点。
白鹭兰
双极定向一个应用就是双极定向的拓扑序前缀和后缀都是联通的。对应着本题中 的情况。于是使用上面的构造过程即可。
现在的问题是要对整张图进行缩点,将它的圆方树缩成一条链。这个可以看作选择一条路径 ,并断掉路径上所有方点,求圆点最多的连通块原点个数最小值。
考虑如果 的 LCA 确定了,接下来每一步都是走按圆点个数剖分的重儿子/次重儿子。这个结论可以简单用调整证明。那么简单 DP 就可以求出 本题就解决了。
代码细节略多。
#include <cstdio> #include <vector> using namespace std; int read(){ char c=getchar();int x=0; while(c<48||c>57) c=getchar(); do x=(x<<1)+(x<<3)+(c^48),c=getchar(); while(c>=48&&c<=57); return x; } const int N=400003,M=460003,INF=0x3f3f3f3f; int n,m,cnt; int hd[N],ver[M<<1],nxt[M<<1],tot; void chmx(int &x,int v){if(x<v) x=v;} void chmn(int &x,int v){if(x>v) x=v;} void add(int u,int v){ nxt[++tot]=hd[u];hd[u]=tot;ver[tot]=v; } int p[N],loc; namespace bipolar{ int hd[N],ver[M<<1],nxt[M<<1],tot; void add(int u,int v){ nxt[++tot]=hd[u];hd[u]=tot;ver[tot]=v; } int dfn[N],low[N],num; vector<int> vec[N]; int fa[N],deg[N]; int sink; int chain[N],len; bool dfs(int u){ bool fl=(u==sink); dfn[u]=low[u]=++num; for(int i=hd[u];i;i=nxt[i]){ int v=ver[i]; if(dfn[v]) chmn(low[u],dfn[v]); else{ fa[v]=u; ++deg[u]; fl|=dfs(v); chmn(low[u],low[v]); } } if(fl) chain[++len]=u; return fl; } int que[N],tl; bool del[N]; void proc(int u){ if(del[u]) return; p[++loc]=u; del[u]=1; for(int x:vec[dfn[u]]) proc(x); } void orient(int s,int t,int rk){ sink=t;dfs(s); for(int i=1;i<=rk;++i) if(!deg[i]) que[++tl]=i; for(int pos=1;pos<=tl;++pos){ int u=que[pos]; if(u==t) continue; vec[dfn[fa[u]]].emplace_back(u); vec[low[u]].emplace_back(u); if(!--deg[fa[u]]) que[++tl]=fa[u]; } while(len) proc(chain[len--]); } } namespace bct{ const int N=::N<<1; int hd[N],ver[N<<1],nxt[N<<1],tot; void add(int u,int v){ nxt[++tot]=hd[u];hd[u]=tot;ver[tot]=v; nxt[++tot]=hd[v];hd[v]=tot;ver[tot]=u; } int sz[N],f[N],sn[N]; int res,pos,pa,pb; void dfs(int u,int fa){ int a=0,b=0; sz[u]=(u<=n); for(int i=hd[u];i;i=nxt[i]){ int v=ver[i]; if(v==fa) continue; dfs(v,u); sz[u]+=sz[v]; if(sz[v]>sz[a]) b=a,a=v; else if(sz[v]>sz[b]) b=v; } int cur=(u<=n),tmp=(u<=n); f[u]=f[a]; sn[u]=a; for(int i=hd[u];i;i=nxt[i]){ int v=ver[i]; if(v==fa||v==a) continue; if(u<=n) tmp+=sz[v]; else chmx(tmp,sz[v]); if(v==b) continue; if(u<=n) cur+=sz[v]; else chmx(cur,sz[v]); } if(u<=n) cur+=n-sz[u]; else chmx(cur,n-sz[u]); chmx(cur,f[a]); chmx(cur,f[b]); chmx(f[u],tmp); if(cur<=res){res=cur;pos=u;pa=a;pb=b;} } bool del[N]; int col[N],rk; vector<int> nd[N]; void paint(int u){ if(col[u]||del[u]) return; col[u]=rk; if(u<=n) nd[rk].emplace_back(u); for(int i=hd[u];i;i=nxt[i]) paint(ver[i]); } void solve(){ res=INF;pos=0; dfs(1,0); if(pos>n) del[pos]=1; int pu=pos,pv=pos; for(int i=pa;i;i=sn[i]) if(i>n) del[i]=1;else pu=i; for(int i=pb;i;i=sn[i]) if(i>n) del[i]=1;else pv=i; for(int i=1;i<=n+cnt;++i) if(!col[i]) ++rk,paint(i); for(int u=1;u<=n;++u) for(int i=::hd[u];i;i=::nxt[i]){ int v=::ver[i]; if(col[u]!=col[v]) bipolar::add(col[u],col[v]); } bipolar::orient(col[pu],col[pv],rk); printf("%d %d\n",res,loc); for(int i=1;i<=loc;++i){ printf("%lu ",nd[p[i]].size()); for(int x:nd[p[i]]) printf("%d ",x); putchar('\n'); } } } int dfn[N],low[N],num; int stk[N],tp; void tarjan(int u){ dfn[u]=low[u]=++num;stk[++tp]=u; for(int i=hd[u];i;i=nxt[i]){ int v=ver[i]; if(dfn[v]) chmn(low[u],dfn[v]); else{ tarjan(v); chmn(low[u],low[v]); if(low[v]>=dfn[u]){ int x; ++cnt; do{ x=stk[tp--]; bct::add(n+cnt,x); }while(x!=v); bct::add(n+cnt,u); } } } } int main(){ n=read();m=read(); for(int i=1;i<=m;++i){ int u=read(),v=read(); add(u,v);add(v,u); } for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i),tp=0; bct::solve(); return 0; }
以上参考自 ix35 《综述图论中连通性及相关问题的一些处理方法》与 zx2003 博客。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现