支配树学习笔记
先抛出一个问题:给一个有向图,问从
这里,支配的定义为:若从
那么受支配集意思就是对于
也就是说,这是一个树形结构。每个点的父亲为第一个支配他的点,那么这棵支配树中每个点的祖先和自己都是支配自己的点。现在,我们考虑求出这棵支配树。
考虑运用 dfs 序和生成树完成该问题,为了方便,以下的任意节点
于是发明支配树的强者建立了一个新的概念:半支配点。如果
我们先设点
结论 0:
显然能到达
结论 1:
证明:考虑
结论 2:
在生成树中,
证明:考虑到从根到
结论 3:
在生成树中,
证明:
结论 4:
在生成树中,
证明:显然。
通过这些结论,我们可以得到第一个定理:
证明:前一半是显然的,并且我们可以知道右边的式子显然要大于等于
然后,我们在半支配路径
所以该式子中包含了
于是,我们便有了一种快速求解半支配点的方法。我们按照 dfs 序从后往前做,利用带权并查集维护刚刚的式子即可。
求出来半支配点,我们就可以求解支配树了!
首先有个非常丑陋的方法,也比较难写,非常不推荐。
就是我们在生成树基础上将
首先在建立生成树时,如果存在从
我们只考虑证明先经过一段树边,再经过一段非树边到
所以,我们可以在这样一个 DAG 中求出支配树。
而第二种方法才是主流方法。这里需要另外两个定理。
定理 1:对于任意节点
(注意!链是指树上的链,我学这个内容时理解错了,卡了很久!)
证明:考虑
定理 2:对于任意节点
证明:首先,
于是,我们发现这样子就可以转移了。支配树的代码实现非常容易,我们整个按 dfs 序从后往前转移,多么的无私啊,具体实现看代码:
#include <bits/stdc++.h>
#define rep(i, l, r) for (int i = l; i <= r; ++ i)
#define rrp(i, l, r) for (int i = r; i >= l; -- i)
#define eb emplace_back
#define inf 1000000000
#define linf 100000000000000
#define pii pair <int, short>
using namespace std;
constexpr int N = 2e5 + 5, P = 998244353;
inline int rd ()
{
int x = 0, f = 1;
char ch = getchar ();
while (! isdigit (ch)) { if (ch == '-') f = -1; ch = getchar (); }
while (isdigit (ch)) { x = (x << 1) + (x << 3) + ch - 48; ch = getchar (); }
return x * f;
}
int n, m;
vector <int> g[N], fg[N], vec[N];
int dfn[N], id[N], tim;
int idom[N], sdom[N], fa[N];
struct DSU
{
int fa[N], mn[N];
void init ()
{
rep (i, 1, n) fa[i] = mn[i] = i;
}
int find (int x)
{
if (x == fa[x]) return x;
int fx = find (fa[x]);
if (dfn[sdom[mn[x]]] > dfn[sdom[mn[fa[x]]]]) mn[x] = mn[fa[x]];
return fa[x] = fx;
}
} d;
void dfs (int u)
{
id[dfn[u] = ++ tim] = u;
for (auto v : g[u]) if (! dfn[v]) dfs (v), fa[v] = u;
}
int ans[N];
int main ()
{
n = rd (), m = rd ();
rep (i, 1, m)
{
int u = rd (), v = rd ();
g[u].eb (v);
fg[v].eb (u);
}
dfs (1);
rep (i, 1, n) sdom[i] = i;
d.init ();
rrp (i, 1, n)
{
int u = id[i];
if (! u) continue;
for (auto v : vec[u])
{
d.find (v);
if (sdom[d.mn[v]] == u) idom[v] = u;
else idom[v] = d.mn[v]; //为什么可以这样?考虑到最后的点一定会指向自己的sdom,递归过来即可
}
if (i == 1) continue;
for (auto v : fg[u])
{
if (! dfn[v]) continue;
if (dfn[v] < dfn[sdom[u]]) sdom[u] = v;
else
{
d.find (v);
if (dfn[sdom[u]] > dfn[sdom[d.mn[v]]]) sdom[u] = sdom[d.mn[v]];
}
}
vec[sdom[u]].eb (u);//建立半支配树
d.fa[u] = fa[u];//更新父亲,以便求祖先的最小sdom
}
rep (i, 1, n) if (idom[id[i]] ^ sdom[id[i]]) idom[id[i]] = idom[idom[id[i]]];
rrp (i, 2, n)
{
++ ans[id[i]];
ans[idom[id[i]]] += ans[id[i]];
}
++ ans[1];
rep (i, 1, n) printf ("%d ", ans[i]);
}
于是,我们便学会了支配树。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?