支配树, Tarjan: 没错又是我
支配树 (Dominator Tree)
小贴士
由于一些原因, 支配树的中文条目和英文条目有一些出入, 不同的地方在中文词条是不能自恰的, 在这里提醒一下. 写这段文字时的版本是这个 (可能要复制链接访问而不是单击访问), 表格中 号点没有 idom, 而按照左边的定义和英文条目来看, 号点的 idom 是 , 也应该是 .
一些定义
对于 支配 , 我们称 是 的必经点 (dom). 点 的最近必经点 (idom) 必须满足除了它本身外不支配任何支配 的节点. 记 表示 的 idom.
预处理
我们一开始从 点开始 DFS, 记录每个点的 DFS 序 (DFN).
半必经点
定义 为 的半必经点 (semi), 需要存在 到 的一条路径上除了两端的 , , 剩下的任何点 都满足 . 我们记 表示 的 DFN 最小的 semi.
每个点 DFS 树上的父亲一定是它的一个 semi. 这样就可以推出 一定是 的一个祖先. 如果 不是 的父亲, 则它不是 的祖先就是和 无直系关系, 在另一棵更先搜索的子树上. 但是后者矛盾是因为如果存在一条路径从更先搜索的 到 , 那么 也会比它的父亲更先搜索. 因此 一定是祖先.
如果存在边 , 并且 , 也就是说这是一条 DFS 树上的树边, 是 的父亲, 则 显然是 的一个 semi.
如果 , 那么这是横叉边或回边, 那么 和它满足 的祖先 的 也是 的 semi.
设点 是 或它的祖先, 满足 . 我们一定可以找到一条 到 的树边路径, 然后接上 变成 到 的路径, 根据定义能找到一条 到 的路径, 路径上其它点 的 . 拼接起来得到 到 的路径. 如果存在环, 那么我们只截取 到 的部分. 无论 是回边还是横叉边, 那么这路径中间的节点 一定满足 , 所以也是 的 semi.
推论可以简化为: , 的 LCA 的子树中的 的祖先 (包含 , 不包含 LCA) 的 中 DFN 最小的一个.
求每个点 DFN 最小的 semi
为了求出每个点的 , 我们按 DFS 序倒着枚举, 对每个点枚举它的所有入边, 如果是父亲, 那么就直接更新 , 如果是横叉边或回边, 那么这个前驱点一直到 LCA 路径上所有点 (不包含 LCA) 的 应该已经算完了, 并且 LCA 和它祖先的 一定没算, 我们用并查集维护每个点到根的路径上, 所有确定 值的点的 最小值, 这样可以 对横叉边和回边进行查询.
求每个点的 idom
一定是 或它的祖先. 因为 支配 , 因此一定是 的祖先.
当 是 在 DFS 树上的父亲的时候, 同为 祖先的 一定满足不是 就是它的祖先的条件.
当 不是 DFS 树上的父亲的时候, 可以找到包含非树边的路径到 , 因此这时 不可能是 的后代.
我们找到 到 路径上, 除 以外的所有点, 最小的是 .
如果能够找到路径绕开 , 则一定有 . 所以如果 的情况, 就有 , 又因为 是 或它的祖先, 因此 .
如果真的可以绕开, 那么支配 的必要条件就是支配 . 又因为 一定是绕开 的路径中, 离开树边最早的点, 因此支配 也是支配 的充分条件. 综上, .
快速求
我们发现在求 的时候我们也维护了树上一条链的 最小的 . 每次求 的 之前, 一定已经求出了 DFS 树上 的所有后代的 , 也就是说所有以 作为 的点都被求出来了. 这时遍历所有 的 , 用并查集查到的便是 的 值.
最后通过每个点的 值按 DFN 从小到大扫一遍, 进行上面对 的判断然后给 赋值即可.
最后是代码, 这就是面向 vector
编程.
unsigned m, n;
unsigned A, B, C, D, t;
unsigned Cnt(0), Ans(0), Tmp(0);
struct Node;
struct Set {
Set* Fa;
Node* Val;
}S[200005], *Stack[200005], **STop(Stack);
struct Node {
vector<Node*> E, IE, DFSSon, SemSon, Son;
Node *Sem, *Fa, *J;
unsigned DFN, Size;
inline void DFSforDFN();
inline void DFSforSize();
inline char Les(Node* x) {return Sem->DFN < x->Sem->DFN;}
}N[200005], *Rk[200005];
inline Node* Find(Set* x) {
while (x != x->Fa) *(++STop) = x, x = x->Fa;
Node* Cur(x->Val);
while (STop > Stack)
Cur = ((*STop)->Val = (Cur->Les((*STop)->Val)) ? Cur : (*STop)->Val), (*(STop--))->Fa = x;
return Cur;
}
inline void Node::DFSforDFN() {
Rk[DFN = ++Cnt] = this;
for (auto i:E) if(!(i->DFN)) DFSSon.push_back(i), i->DFSforDFN();
}
inline void Node::DFSforSize() {
Size = 1;
for (auto i:Son) i->DFSforSize(), Size += i->Size;
}
signed main() {
n = RD(), m = RD();
for (unsigned i(1); i <= m; ++i)
A = RD(), B = RD(), N[A].E.push_back(N + B), N[B].IE.push_back(N + A);
N[1].DFSforDFN(), N[n + 1].DFN = 0x3f3f3f3f;
for (unsigned i(1); i <= n; ++i) S[i] = {S + i, N + i};
for (unsigned i(1); i <= n; ++i) N[i].J = N + i;
for (unsigned i(n); i; --i) {
Node* Cur(Rk[i]);
for (auto j:Cur->SemSon) {
Node* Get(Find(S + (j - N)));
if(Get->Les(j->J)) j->J = Get;
}
Cur->Sem = N + n + 1;
for (auto j:Cur->IE) {
if(j->DFN < Cur->DFN) Cur->Sem = (Cur->Sem->DFN > j->DFN) ? j : Cur->Sem;
else {
Node* Get(Find(S + (j - N)));
Cur->Sem = (Get->Les(Cur)) ? Get->Sem : Cur->Sem;
}
}
Cur->Sem->SemSon.push_back(Cur);
for (auto j:Cur->DFSSon) S[j - N].Fa = S + (Cur - N);
}
for (unsigned i(1); i <= n; ++i) {
Node* Cur(Rk[i]);
if(Cur->J->Sem == Cur->Sem) Cur->Fa = Cur->Sem;
else Cur->Fa = Cur->J->Fa;
}
for (unsigned i(2); i <= n; ++i) N[i].Fa->Son.push_back(N + i);
N[1].DFSforSize();
for (unsigned i(1); i <= n; ++i) printf("%u ", N[i].Size); putchar(0x0A);
return Wild_Donkey;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)