【前言】
- 本文为博主的转载,由于博主看到的文章同样是转载的,无法注明原文出处。
- 博主在原文的基础上修改了格式、措辞和一些小错误,并适当添加了一些自己的理解。
【支配树简介】
对于一个单源有向图上的每个点 ,都存在点 满足去掉 之后起点无法到达 ,我们称作 支配 , 是 的一个支配点。
支配 的点可以有多个,但是至少会有一个。显然,对于起点以外的点,它们都有两个平凡的支配点,一个是自己,一个是起点。
在支配 的点中,如果一个支配点 满足 被 剩下的所有支配点支配,则这个 称作 的最近支配点 ,记作 。
定理 :我们把图的起点称作 ,除 以外每个点均存在唯一的 。
证明:如果 支配 且 支配 ,则 一定支配 ,因为到达 的路径都经过了 所以必须经过 。如果 支配 且 支配 ,则 支配 (或者 支配 ),否则存在从 到 再到 的路径绕过 ,与 支配 矛盾。这就意味着支配定义了点 的支配点集合上的一个全序关系,所以一定可以找到一个“最小”的元素使得所有元素都支配它。
于是, 的边,就能得到一棵树,其中每个点支配它子树中的所有点,它就是支配树。
下文介绍了构建支配树的 算法,其时间复杂度为 ,空间复杂度为 。
为了能够求出支配树,我们下面来推导一下需要用到的一些定理。
【定理推导】
首先,我们会使用一棵 树来帮助我们计算。从起点出发进行 就可以得到一棵 树。
原图中的边被分为了以下两类:在 树上出现的边称作树边,剩下的边称为非树边。非树边也可以分为几类:从祖先指向后代(前向边),从后代指向祖先(后向边),从一棵子树內指向另一棵子树内(横叉边)。树边是我们非常熟悉的,所以着重考虑一下非树边。
我们按照 到的先后顺序给点从小到大编号(在下面的内容中我们通过这个比较两个节点),那么前向边总是由编号小的指向编号大的,后向边总是由大指向小,横叉边也总是由大指向小。现在在 树上我们要证明一些重要的引理:
引理 (路径引理):如果两个点 满足 ,那么任意 到 的路径经过 的公共祖先。(注意这里不是说 )
证明:如果 其中一个是另一个的祖先显然成立。否则删掉起点到 路径上的所有点(这些点是 的公共祖先),那么 和 在两棵子树内,并且因为公共祖先被删去,无法通过后向边到达子树外面,前向边也无法跨越子树,而横叉边只能从大到小,所以从 出发不能离开这颗子树到达 。所以如果本来 能够到达 ,就说明这些路径必须经过 的公共祖先。
在继续之前,我们先约定一些记号:
代表图的点集, 代表图的边集。 代表从点 直接经过一条边到达点 , 代表从点 经过某条路径到达点 , 代表从点 经过 树上的树边到达点 ( 是 在 树上的祖先), 代表 且 。
定义:半支配点 :对于 ,它的半支配点定义为 1 1
这个定义可以理解为从 出发,可以绕过小于 的所有点到达 (只能以大于 的点作为落脚点)的最小的 。
注意这只是个辅助定义,并不是真正的支配点。
引理 :对于任意 ,有 。
证明:如果不是这样的话就可以直接通过树边不经过 就到达 了,与 定义矛盾。
引理 :对于任意 ,有 。
证明:对于 在 树上的父亲 , 这条路径只有两个点,所以满足 定义中的条件,于是它是 的一个候选。所以 。在这里我们就可以使用路径引理证明 不可能在另一棵子树,因为如果是那样的话就会经过 和 的一个公共祖先,公共祖先的编号一定小于 ,所以不可行。于是 就是 的真祖先。
引理 :对于任意 ,有 。
证明:如果不是这样的话,按照 的定义,就会有一条路径是 不经过 了,与 定义矛盾。
引理 :对于满足 的点 , 或 。
直观地理解就是 到 的路径两两之间边不相交或者存在完全包含关系。
证明:如果不是这样的话,就说明 ,那么存在路径 不经过 到达了 (因为 是 的真后代,一定不支配 ,所以存在绕过 到达 的路径),矛盾。
上面这 条引理都比较简单,但是非常重要的性质。接下来我们要证明几个定理,它们揭示了 与 的关系。证明会比上面的复杂一点。
定理 :对于任意 ,如果所有满足 的 也满足 ,那么 。
证明:条件可以写为 。
由上面的引理 知道 ,所以只要证明 支配 就可以保证是最近支配点了。对任意 到 的路径,取上面最后一个编号小于 的 (如果 就是 的话显然定理成立),它必然有个后继 满足 (否则 会变成 ),我们取最小的那个 。同时,如果 不是 ,根据条件, ,所以 不可能是 ,这就意味着 到 的路径上一定有一个 满足 ,因为 是小于 的最后一个,所以 也满足 ,但是我们取的 已经是最小的一个了,矛盾。于是 只能是 ,那么我们就证明了对于任意路径都要经过 ,所以 就是 。
定理 :对于任意 ,令 为所有满足 的 中 最小的一个,那么 。
证明:条件可以写为 。
由引理 ,有 或 ,由引理 排除后面这种。所以只要证明 支配 即可。类似定理 的证明,我们取任意 到 路径上最后一个小于 的 (如果 是 的话显然定理成立),路径上必然有个后继 满足 (否则 会变成 ),我们取最小的一个 。类似上面的证明,我们知道 到 的路径上不能有点 满足 ,于是 成为 的候选,所以 。那么根据条件我们也知道了 不能是 的真后代,于是 满足 。但是我们注意到因为 ,存在一条路径 ,如果 不是 的话这就是一条绕过 的到 的路径,矛盾,所以 必定是 。所以任意到 的路径都经过 ,所以 。
完成了上面两个定理的证明,我们就能够通过 求出 了。
推论 :对于 ,令 为所有满足 的 中 最小的一个,有
推论 可以通过定理 和定理 可以直接得到。这里一定有 ,因为 也是 的候选。
接下来我们的问题是,直接通过定义计算 很低效,我们需要更加高效的方法,所以我们证明下面这个定理:
定理 :对于任意
证明:令右侧为 ,显然右侧的点集中都存在路径绕过 之前的点,所以 。然后我们考虑 到 的绕过 之前的点的路径,如果只有一条边,那么必定满足 且 ,所以此时 ;如果多于一条边,令路径上 的上一个点为 ,我们取路径上除两端外满足 的最小的 (一定能取得这样的 ,因为 是 的候选)。因为这个 是最小的,所以 到 的路径必定绕过了 之前的所有点,于是 是 的候选,所以 。同时, 还满足右侧的条件( 在绕过 之前的点的路径上,于是 ,并且 ,同时 直接连到了 ),所以 是 的候选, 。所以 , 。综上, 且 ,所以 。
现在最困难的步骤已经完成了,我们得到了 的一个替代定义,而且这个定义里面的形式要简单得多。这种基本的树上操作我们是非常熟悉的,所以没有什么好担心的了。接下来就可以给出我们需要的算法了。
【构造流程】
以下是算法的简要流程
、初始化、跑一遍 得到 树和标号
、按标号从大到小求出 (利用定理 )
、通过推论 求出所有能确定的 ,剩下的点记录下和哪个点的 是相同的
、按照标号从小到大再跑一次,得到所有点的以下是算法的具体实现细节
大致要维护的东西:
标号为 的点
有边直接连到 的点集
为点 的点集
在 树上的父亲
以及 和 数组这里多说一句,由于我们上文的推导中我们经常会用一个点的 序来代替一个点来论述,我们有时会无法分清一个数组的下标和数值究竟代表一个点 在原图中的标号还是它的 序。
为了统一,我们不妨规定数组的下标一律使用点在原图中的标号,除了计算过程中的 和 数组记录点的 序标号以外,其余数组一律记录点在原图中的标号。
算法的第 步没什么特别的,规规矩矩地 一次即可,同时初始化 为自己(这是为了实现方便)。
第 、第 步可以一起做。通过一个辅助数据结构维护一个森林,支持加入一条边 和查询点到根路径上的点的 的最小值对应的点 。那么我们求每个点的 只需要对它的所有直接前驱 一次,求得前驱中的 最小值即可。因为定理 中的第一类点编号比它小,它们还没有处理过,所以自己就是根, 就能取得它们的值;对于第二类点, 查询的就是满足 的 的 的最小值。所以这么做和定理 是一致的。
然后把该点加入它的 的 里,连上它与父亲的边。现在它父亲到它的这棵子树中已经处理完了,所以可以对父亲的 里的每个点求一次 并且清空 。对于 里的每个点 ,求出 ,此时 ,于是直接按照推论 ,如果 ,则 ;否则可以记下 ,实现时我们可以写成 ,留到第 步处理。
最后从小到大扫一遍完成第 步,对于每个 ,如果 的话,就已经是第 步求出的正确的 了,否则就证明这是第 步留下的待处理点,令 即可。
至于这个辅助数据结构,我们可以选择并查集。不过因为我们需要查询到根路径上的信息,所以不方便按秩合并,但是我们仍然可以路径压缩,压缩时保留路径上的最值就可以了。这样做的话,最终的时间复杂度是 。
【代码】
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 50005;
template <typename T> void chkmax(T &x, T y) {x = max(x, y); }
template <typename T> void chkmin(T &x, T y) {x = min(x, y); }
template <typename T> void read(T &x) {
x = 0; int f = 1;
char c = getchar();
for (; !isdigit(c); c = getchar()) if (c == '-') f = -f;
for (; isdigit(c); c = getchar()) x = x * 10 + c - '0';
x *= f;
}
template <typename T> void write(T x) {
if (x < 0) x = -x, putchar('-');
if (x > 9) write(x / 10);
putchar(x % 10 + '0');
}
template <typename T> void writeln(T x) {
write(x);
puts("");
}
int n, m, timer, root, dfn[MAXN], p[MAXN], father[MAXN];
int idom[MAXN], sdom[MAXN], f[MAXN], home[MAXN];
long long ans[MAXN]; vector <int> a[MAXN], b[MAXN], c[MAXN];
void dfs(int pos) {
dfn[pos] = ++timer, p[timer] = pos;
for (unsigned i = 0; i < a[pos].size(); i++)
if (dfn[a[pos][i]] == 0) {
father[a[pos][i]] = pos;
dfs(a[pos][i]);
}
}
int F(int x) {
if (f[x] == x) return x;
int tmp = f[x];
f[x] = F(f[x]);
if (sdom[home[tmp]] < sdom[home[x]]) home[x] = home[tmp];
return f[x];
}
int gethome(int x) {
F(x);
return home[x];
}
void work(int pos, long long sum) {
ans[pos] = sum;
for (unsigned i = 0; i < a[pos].size(); i++)
work(a[pos][i], sum + a[pos][i]);
}
int main() {
while (scanf("%d%d", &n, &m) != EOF) {
for (int i = 1; i <= n; i++) {
a[i].clear();
b[i].clear();
c[i].clear();
}
for (int i = 1; i <= m; i++) {
int x, y; read(x), read(y);
a[x].push_back(y);
b[y].push_back(x);
}
memset(dfn, 0, sizeof(dfn));
timer = 0; dfs(root = n);
for (int i = 1; i <= timer; i++) {
sdom[p[i]] = i;
idom[p[i]] = 0;
f[p[i]] = home[p[i]] = p[i];
}
for (int i = timer; i >= 2; i--) {
int tmp = p[i];
for (unsigned j = 0; j < b[tmp].size(); j++)
if (dfn[b[tmp][j]]) chkmin(sdom[tmp], sdom[gethome(b[tmp][j])]);
c[sdom[tmp]].push_back(tmp);
f[tmp] = father[tmp];
tmp = dfn[father[tmp]];
for (unsigned j = 0; j < c[tmp].size(); j++) {
int tnp = gethome(c[tmp][j]);
if (sdom[tnp] == tmp) idom[c[tmp][j]] = tmp;
else idom[c[tmp][j]] = dfn[tnp];
}
}
for (int i = 1; i <= n; i++)
a[i].clear();
for (int i = 2; i <= timer; i++) {
int tmp = p[i];
if (sdom[tmp] == idom[tmp]) idom[tmp] = p[idom[tmp]];
else idom[tmp] = idom[p[idom[tmp]]];
sdom[tmp] = p[sdom[tmp]];
a[idom[tmp]].push_back(tmp);
}
memset(ans, 0, sizeof(ans));
work(root, root);
for (int i = 1; i <= n; i++) {
write(ans[i]);
if (i == n) putchar('\n');
else putchar(' ');
}
}
return 0;
}