支配树

· · 算法·理论

## “支配” + $x$ 支配 $y$($xDy$):从有向图上一个**固定起点** $s$ 出发,到 $y$ 的路径全部经过 $x$。多个起点的情况可以建超级源。 + 支配是一种偏序关系。 + 如果对每个 $y$ 只保留最近的 $x$,支配关系可以形成一棵树,称其为支配树。$x$ 称为 $y$ 的直接支配点 $idom_y$。 容易 $O(n^2)$ 构建一般图的支配树。 DAG 的支配树构建:按拓扑序遍历,每次一个点的 $idom$ 就是,它在原图上的所有前驱在支配树上的 LCA。正确性显然。 ## Lengauer-Tarjan 算法构建一般图支配树 ### 一些定义和性质 先以起点为根建一棵 dfs 树。把所有点按 dfn 重标号。以下“祖先”等概念均在 dfs 树上定义。 + 横叉边或返祖边 $x\to y$ 一定满足 $x>y$。 + $idom_x$ 是 $x$ 的祖先。 + 若 $x<y$,那么 $x\too y$ 的所有路径一定都经过了 $lca(x,y)$。 + $x$ 每次通过返祖边或横叉边上移都不可能让它们的 lca 变大。 定义 $x$ 的半支配点 $sdom_x$:最小的点满足,存在一条路径从 $sdom_x\too x$,且路径上除两端外的所有点都 $>x$。称一条满足上述条件的路径为好路径。 + $sdom_x<x$ 且 $sdom_x$ 是 $x$ 的祖先。 + $sdom_x\too x$ 的树上路径中,除两端外的所有点都不可能成为 $x$ 及其后代的支配点。 + $idom_x$ 一定是 $sdom_x$ 的祖先或者 $sdom_x$ 本身。 ### 求解半支配点 $$sdom_x=\min(\{u\mid u<x,(u,x)\in E\}\bigcup\{sdom_y\mid y>x,\exist u:(u,x)\in E,y\in anc(u)\})$$ 也就是说 $sdom_x$ 为此两者的最小值: + $<x$ 且有 $u\to x$ 连边的 $u$。 + 满足“点 $y>x$ 且 $y$ 为某个有 $u\to x$ 连边的点 $u$ 的祖先”的 $sdom_y$。 > 对于前者,显然处理了 $sdom_x\too x$ 没有中间点的情况。 > > 否则在后者处理。这么做的想法是考虑 $sdom_x\too x$ 中最后一条边 $v\to x$,$v>x$。用 $sdom_v$ 直接更新可以保证合法性,但是会漏解,因为这样只考虑了 $sdom_x\too x$ 路径上点 $\ge v$ 的情况,而要求只是 $>x$。 > > 考虑 $sdom_x\too x$ 的最小点实际上出现在什么地方,设其为 $y$。那它应满足 $x<y\le v$。事实上 $y$ 一定是 $v$ 的祖先。否则,$y\too v$ 一定会经过 $lca(y,v)\le y$,和 $y$ 的最小性矛盾。那么就得到了上述式子。 于是可以快速实现半支配点 $sdom_x$ 的求解:倒序遍历 $n\to 1$,用带权并查集维护每个点的祖先中已经更新过的 $sdom$ 最小值。 ### 求解直接支配点 有一种直接的做法是,直接在 dfs 树上添加所有 $sdom_x\to x$ 得到一个 DAG,对此 DAG 求支配树即可。不过普遍不这么做。 考虑之前的这么一条性质:“$sdom_x\too x$ 的树上路径中,除两端外的所有点都不可能成为 $x$ 及其后代的支配点。” 也就是说对于点 $x$,$y$ 为 $x$ 的祖先,如果存在 $u$ 为 $x$ 的祖先(或 $x$ 自身),使点 $y$ 被 $sdom_u\too u$ 的树上路径(不含两端)覆盖,那么 $y$ 就不可能成为点 $x$ 的支配点。 那反过来,如果 $y$ 没有被任何一个 $sdom_u\too u$ 覆盖,那它是不是就可以作为 $x$ 的支配点了呢? 这是对的。 > 考虑如果存在不经过 $y$ 的路径 $s\too x$,那这样的路径中一定有满足如下形式的:在 $s\too x$ 的树上路径中,$u,v$ 为 $y$ 两侧的某两个点,那么路径形如 $s\too u\too v\too x$。其中 $u\too v$ 除 $u,v$ 外,没有经过 $s\too x$ 树上路径中的任何一个点。 > > 如果 $u\too v$ 这一段经过了除 $u$ 外 $<v$ 的点 $z$,那根据之前的性质,它在走到 $v$ 之前一定到过 $lca(v,z)$。这不符合上面的限制。否则,$y$ 就被 $sdom_v\too v$ 盖住了,矛盾。 于是一个易于求解的描述形式出现了:设 $v$ 为 $sdom_x\to x$(不含 $sdom_x$)上 $sdom_v$ 最小的点。有: $$idom_x=\begin{cases}sdom_x&sdom_v=sdom_x\\idom_v&\text{otherwise.}\end{cases}$$ 实现方面,$v$ 的预处理可以在 $sdom$ 的部分一起做。把每个 $i$ 的求 $v$ 挂到 $sdom_i$ 上。 ### 代码 ```cpp ll n,m,dfn[N],cnn,co[N],fa[N],sd[N],id[N],sz[N]; vector<ll> e[N],e1[N],qv[N]; void dfs(ll p) { dfn[p]=++cnn,co[cnn]=p; for (ll i:e[p]) if (!dfn[i]) fa[i]=p,dfs(i); } struct dsu { ll f[N],ps[N]; void ini(ll n,ll *a) { iota(f+1,f+n+1,1); for (ll i=1;i<=n;i++) ps[i]=a[i]; } ll fnd(ll x) { if (f[x]==x) return x; ll ff=f[x]; f[x]=fnd(f[x]); if (sd[ps[ff]]<sd[ps[x]]) ps[x]=ps[ff]; return f[x]; } ll get(ll x) { return fnd(x),ps[x]; } void mrg(ll x,ll y) { f[fnd(x)]=fnd(y); } } D; void mian() { scanf("%lld%lld",&n,&m); for (ll i=1,x,y;i<=m;i++) { scanf("%lld%lld",&x,&y); if (x!=y) e[x].pb(y),e1[y].pb(x); } dfs(1),D.ini(n,co); for (ll i=1;i<=n;i++) sd[i]=J; for (ll ii=n;ii;ii--) { ll i=co[ii]; for (ll j:qv[i]) id[j]=D.get(dfn[j]); sd[i]=ii; if (ii==1) break; for (ll j:e1[i]) { if (dfn[j]<dfn[i]) sd[i]=min(sd[i],dfn[j]); else sd[i]=min(sd[i],sd[D.get(dfn[j])]); } D.mrg(dfn[i],dfn[fa[i]]),qv[co[sd[i]]].pb(i); } for (ll ii=2;ii<=n;ii++) { ll i=co[ii]; id[i]=sd[id[i]]==sd[i]?sd[i]:id[id[i]]; } for (ll ii=n;ii;ii--) { ll i=co[ii]; sz[i]++,sz[co[id[i]]]+=sz[i]; } for (ll i=1;i<=n;i++) cout<<sz[i]<<" "; } ```