树上启发式合并 学习笔记

作者: xht37 分类: 笔记 发布时间: 2019-12-28 11:45

Visits: 545

树上启发式合并 (dsu on tree) 可以在较优秀的复杂度内解决某些树上离线问题。

引入

考虑这样一个问题:

  • 给一棵 $n$ 个点的有根树,求每棵子树的颜色种类数。

考虑暴力,当我们要求 $x$ 的子树的答案时,我们可以用一个 $c$ 数组记录每个颜色出现的次数,将 $x$ 子树遍历一遍后,$c$ 数组中不为 $0$ 的位置的个数就是 $x$ 子树的颜色种类数,然后清空 $c$ 数组。

显然这样总复杂度是 $\mathcal O(n^2)$ 的,有没有办法优化一下呢?

dsu on tree

当我们求完 $x$ 子树的答案后,如果我们立刻要求 $fa_x$ 子树的答案,那么我们就不必清空 $c$ 数组,同时遍历 $fa_x$ 子树时也不需要遍历 $x$ 子树。

看起来这样似乎并没有优化多少?

那么,如果我们对于每个点 $x$,在求完其重儿子 $son_x$ 的答案后,立刻按照上述方法求 $x$ 的答案,你可以惊奇的发现时间复杂度被优化到了 $\mathcal O(n \log n)$!

具体实现见代码:

const int N = 1e5 + 7;
int n, m, a[N], s[N], son[N], dfn[N], num, c[N], now, ans[N];
vi e[N];

void dfs(int x, int f) {
    s[x] = 1, dfn[x] = ++num;
    for (auto y : e[x]) if (y != f) dfs(y, x), s[x] += s[y], son[x] = s[y] > s[son[x]] ? y : son[x];
}

inline void add(int x) {
    now += !c[x]++;
}

void dfs(int x, int f, bool k) {
    for (auto y : e[x]) if (y != f && y != son[x]) dfs(y, x, 0);
    if (son[x]) dfs(son[x], x, 1);
    for (auto y : e[x]) if (y != f && y != son[x]) for (int i = 0; i < s[y]; i++) add(a[dfn[y]+i]);
    add(a[dfn[x]]), ans[x] = now;
    if (!k) for (int i = now = 0; i < s[x]; i++) c[a[dfn[x]+i]] = 0;
}

int main() {
    rd(n);
    for (int i = 1, x, y; i < n; i++) rd(x), rd(y), e[x].pb(y), e[y].pb(x);
    dfs(1, 0);
    for (int i = 1; i <= n; i++) rd(a[dfn[i]]);
    dfs(1, 0, 1), rd(m);
    for (int i = 1, x; i <= m; i++) rd(x), print(ans[x]);
    return 0;
}

证明

首先我们知道一个点到根节点上有 $\mathcal O(\log n)$ 条重链和 $\mathcal O(\log n)$ 条轻边。

注意到每条轻边会导致下面的子树被多遍历一次,因此每个点最多被遍历 $\mathcal O(\log n)$ 次。

因此总时间复杂度为 $\mathcal O(n \log n)$。

【例题】CF600E Lomsat gelral

求子树众数和,就在上面代码的基础上改一下即可。

const int N = 1e5 + 7;
int n, a[N], b[N], s[N], son[N], dfn[N], num, c[N], mx;
ll now, ans[N];
vi e[N];

void dfs(int x, int f) {
    s[x] = 1, dfn[x] = ++num;
    for (auto y : e[x]) if (y != f) dfs(y, x), s[x] += s[y], son[x] = s[y] > s[son[x]] ? y : son[x];
}

inline void add(int x) {
    if (++c[x] > mx) mx = c[x], now = 0;
    if (c[x] == mx) now += x;
}

void dfs(int x, int f, bool k) {
    for (auto y : e[x]) if (y != f && y != son[x]) dfs(y, x, 0);
    if (son[x]) dfs(son[x], x, 1);
    for (auto y : e[x]) if (y != f && y != son[x]) for (int i = 0; i < s[y]; i++) add(a[dfn[y]+i]);
    add(a[dfn[x]]), ans[x] = now;
    if (!k) for (int i = mx = now = 0; i < s[x]; i++) c[a[dfn[x]+i]] = 0;
}

int main() {
    rd(n);
    for (int i = 1; i <= n; i++) rd(b[i]);
    for (int i = 1, x, y; i < n; i++) rd(x), rd(y), e[x].pb(y), e[y].pb(x);
    dfs(1, 0);
    for (int i = 1; i <= n; i++) a[dfn[i]] = b[i];
    dfs(1, 0, 1);
    for (int i = 1; i <= n; i++) print(ans[i], " \n"[i==n]);
    return 0;
}

【练习】CF741D Arpa’s letter-marked tree and Mehrdad’s Dokhtar-kosh paths

【练习】CF570D Tree Requests

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注