树分治

树上分治是一个非常重要的东西

这里总结了几种分治方法

链分治

一个问题: 如何定义一种断链方案, 使得断完链后任何一个连通块都是一条链, 且让任何一条从根节点到任意节点的链被段开成的节数最小?

一个比较符合人类直觉的一点是那种经过次数很多的链我们尽可能不去断它

所以就有了轻重链剖分, 其中轻链就是要断开的边, 根据某些复杂度分析, 我们可以得到, 这个断开节数最小值是 \(log\)

链分治是一种基于轻重链剖分的结构

首先是询问所有节点到 \(1\) 的路径长度, 确定深度

然后按照深度依次考虑

对于当前点, 与当前已经确定的树, 我们可以通过询问一个叶子节点与当前点的距离, 确定 \(lca\) 的深度, \(lca\) 的子树就是 当前点应在的位置

我们想要优化这个复杂度, 不难想到, 将 \(lca\) 到当前节点的边视为轻边, 只会询问 \(log\) 次, 于是树剖即可

树上问题转序列问题

使用 \(dfs\) 序可以粗略的完成这个事情, 我们退而求其次, 让一条链在序列上不一定连续, 要求被分割的段数尽可能的小

我们使用链分治就可以做到每一条链都分成 \(log\)

这就是树链剖分

dsu on tree

我们考虑这样一个事情, 如何优美的统计子树中的信息

假设我们统计的信息是一个集合

我们考虑当前节点, 一种比较显而易见的想法是, 把自己的儿子都合并起来

但是, 我们通常合并复杂度无法接受, 但是继承是 \(O(1)\)

所以我们想要在合并中加入继承的操作, 那么继承的操作一定形成了一条链

所以考虑一个节点的信息到根路径的距离上, 重链继承, 轻链合并, 根据上边的分析, 一个点最多合并 \(log\)

这个 \(log\) 比较小, 这就是 dsu on tree

所以 dsu on tree 与链分治是等价的

提供一个不用 \(dfs\) 实现 dsu on tree 的做法

for (int i = 1; i <= n; i++) {
    if (top[i] != i) continue; 
    for (int j = i; j; j = son[j]) 
        sta.push(j);
    while (!sta.empty()) {
        int now = sta.top();
        sta.pop();
        if (!son[now]) continue;
        for (int j = out[son[now]] + 1; j <= out[now]; ) {
            for (int k = j; k < j + size[idfn[j]]; k++) {
                //查询
            }
            int last = j + size[idfn[j]] - 1;
            while (j <= last) {
                //插入
                j++;
            }
        }
    }
}

一种重要思想

我们可以得到答案是 $dis(u, v) + \forall t \in path(u, v), \max_{p \in sub_t} {2 (d_p- d_t) + a_p\Delta} $

这是对于 \(lca\) 子树的节点的, 不是 \(lca\) 子树的节点同理

\(path(u, v)\) 不难想到树链剖分, 一个节点的信息可以通过李超线段树实现, 使用树套树, 但是复杂度难以接受

一方面是插入的值过多, 另一方面是查询时间复杂度过高, 还有一方面是空间可能有那么一点点炸

空间炸所以我们就不想要在线去维护这么多元素, 所以离线下来, 每次查询的区间是一段重链上的, 所以我们把询问挂到重链上, 每一次处理一段重链

插入的值过多如何解决呢

我们发现我们询问一段重链时, 有一些值是没有用的, 除了链低我们只用维护轻儿子, 所以这一部分可以使用 dsu on tree 降到一个比较低的插入复杂度, 链低我们单独处理, 不是一段区间, 所以我们可以使用 李超线段树合并 \(dfs\) 解决

第三个是查询, 我们发现我们离线下来后, 有一个非常优美的性质, 大部分是一个前缀, 所以没有必要开线段树来维护区间, 所以可以去掉一个 \(log\) 但是还有一段不是, 我们可以单独处理

这里可以使用线段树套李超线段树解决也是 \(nlog^2n\)

但是我们不用在线, 也没有待修改, 所以没有必要使用数据结构

但是我们也可以使用二区间合并(猫树)来解决

代码 tips:

由于树链剖分与 dsu on tree 的关系, 我们可以很容易的遍历轻儿子就是区间 \([out_{son_i}+1, out_i]\)


用来引入一种非常重要的思想, 在处理链的时候, 可以把链拆成\(log\)条重链区间和\(log\)个点

然后对于这两部分分别求解, 一般 \(log\) 条重链可以只用维护轻子树, 所以可以使用 dsu on tree 或者使用在线维护的方法

上边的蜘蛛爬树其实直接按照这种方式是很不好维护的, 所以不带修采用了离线的方法, 优化了很多东西

把链加拆成上边的东西, 每个节点用平衡树只维护自己的轻儿子, 链加成为 \(log\) 个平衡树插入, 查询把父亲, 重儿子, 自己暴力插入

复杂度 \(O(nlog^2n + nlogn)\)

可以平衡一下复杂度, 设 \(k\) 个重儿子, 复杂度变成 \(O(n log_{k + 1} n log n + klogn)\)

\(k\)\(\frac{logn}{loglogn}\) 时最优

DDP

这是上边思想的一个应用

我们需要平衡修改与查询的操作, 所以我们想到查询的时候算出结果, 而不是保留

我们可以列出 \(DP\) 式子

\[f_{i, 0} = \sum_j \max \{ f_{j, 0}, f_{j, 1} \} \]

\[f_{i, 1} = \sum_j f_{j, 0} + a_i \]

我们考虑一次修改, 只会影响当前节点到根的一条链, 所以我们想到上边的思想, 把他看成若干条重链和若干点

重链我们不维护重儿子信息, 那么修改不会造成影响, 重儿子的信息, 我们通过计算算出, 有新的 \(DP\) 式子

\[f_{i,0} = \max \{f_{j,0} + g_{i,0}, f_{j,1} + g_{i,0}\} \]

\[f_{i, 1} = \max \{ g_{i, 1} + f_{i, 0}, -\infty\} \]

利用斐波那契的思想, 可以把上边的式子写成一个矩阵, 矩阵满足结合律, 然后就可以快速计算了, 修改分情况修改矩阵就可以了

点分治

实际上我们在计算路径的时候, 想要拆成 \(dis_u + dis_v - 2 \times dis_{lca}\)

我们可以只计算 \(dis_{lca}\)\(0\) 的所有情况, 剩下的情况怎么办, 进行点分治

对于点的分治, 考虑我们以一个点为根, 处理出所有经过它的方案, 然后所有不经过它的方案一定在若干个连通块内, 我们发现我们进入了子问题

那么对于根的选取就异常重要, 我们可以选择重心来实现

一个路径只会被统计一次

复杂度是 \(nlogn\)

【模板】点分治 1

点分治一个最重要的点是是否会算重复

一种题是 [省选联考 2020 B 卷] 消息传递

这个可以差分掉, 就是做两遍, 一遍整体做, 一遍对于子树单独做, 差分掉

一种是最优方案 「2017 山东三轮集训 Day7」Easy

可以不用管

如果两个都不行

「2017 山东三轮集训 Day7」Easy

边权可以为负

我们可以使用线段树合并的方法, 合并两颗子树

有时候前后缀合并是不行的, 所以我们可以使用哈夫曼树的方法合并, 因为合并顺序无所谓, 复杂度可以控制在一个 \(log\)

我们发现左右端点有限制, 所以可以对序列分治

然后在线段树上建立虚树, 在虚树上查询直接连边的节点


代码实现上

int root, size[N], max_size[N], sum;
bool del[N];

void getroot (int x, int fa) {
    size[x] = 1; max_size[x] = 0;
    for (auto to : eg[x]) {
        if (to == fa || del[to]) continue;
        getroot (to, x);
        size[x] += size[to];
        max_size[x] = std::max(max_size[x], size[to]);
    }
    max_size[x] = std::max(max_size[x], sum - size[x]);
    if (max_size[x] < max_size[root]) root = x;
}

void solve (int x, int father) {
	int now = sum;

    for (auto to : eg[x]) {
        if (del[to]) continue;
        sum = (size[to] > size[x] ? now - size[x] : size[to]);
        max_size[root = 0] = inf;
        getroot(to, 0);
        solve (root, x);
    }
}

与最小生成树

把边分开分别做最小生成树, 要求边的并是总边数, 然后对于每个最小生成树的边合起来求最小生成树, 可以用点分治来解决这个问题

Tree MST

有根树

如果有根树的话, 对于点分治来说会比较难处理, 但是分析好其实也是可以的

【UR #2】树上GCD

我们第一步先用莫比乌斯差分, 所以求 \(gcd(a, b) \mid i\) 的情况 第一步就不会

然后考虑点分治, 由于它是有根树, 所以需要分情况讨论

第一种是都是子树, 直接预处理出 \(cnt\) 数组然后暴力枚举就可以了, 同一子树内的可以做差分, 复杂度是调和级数 \(n logn\)

第二种是一个是子树, 一个是父亲方面的, 这里的处理方法是枚举 \(lca\) 和求子树一样处理出 \(cnt\) 数组

我们发现我们原先处理的 \(cnt\) 数组不能用了, 但是我们发现我们需要用的是等间距的, 所以我们不难想到根号分治的思想

对于 间距 \(\le \sqrt n\) 的直接分块开数组维护

对于 间距 \(\ge \sqrt n\) 的直接每次暴力查找

复杂度 \(T(n) = T(\frac{n}{2}) + O(n \sqrt n) = O (n \sqrt n)\)

点分树

动态的点分治

我们其实可以先考虑序列上的问题, 我们执行的一次操作是 单点修改, 而分治层数最多是 \(log\) 层, 所以我们可以对于每一个分治层进行修改, 同样的

我们把每一层的重心连起来构成一颗 \(log\) 树高 的一颗虚树, 那么一个修改只会影响 \(log\) 层, 直接暴力跳父亲就可以了

点分治的最重要的一点就是 除杂

有时候可以差分掉

所以点分树维护的时候通常需要维护两个数组, 一个维护子树到自己, 一个维护子树到父亲, 做的时候需要差分掉

多次询问也可以使用点分树来维护

点分树通常维护距离, 数据通常支持可差分

代码实现上, 只需要在点分树的代码中记录父亲就可以了

应用

动态点分治的两个常见用法, 一个是询问树上的某一个点到其他所有点的信息, 另一个是树上二分寻找关键点

我们分别给出例题

我们考虑现实意义, 任意两个点的 \(lca\) 到根的路径长度乘上两个点的点权乘积

我们观察这之中的不变量, 发现 \(dis(i,j)\) 是不变的 所以考虑 \(lca\) 到根的路径用 \(dis(i) + dis(j) - dis(i, j)\) 来实现

\(dis(i, j)\) 是不变的

所以 \(\sum S_i^2 = \sum (sum - S_i) S_i + sum \sum S_i = \sum \sum a_i b_j dis(i, j) + sum \sum S_i\)

我们发现前边与根无关, 后边可以表示成 \(\sum S_i = \sum dis(i, root) a_i\)

\(\sum \sum a_i b_j dis(i, j) = \sum \sum a_i b_j dis(i, j) + \Delta \sum b_j dis(x, j)\)

和上边是一样的, 所以我们考虑如何求解 \(\sum dis(i, root) a_i\)

上点分树

容斥的给出两个东西

\(f_1 = \sum dis(x, i) a_i\)

\(f_2 = \sum dis(fa_x, i) a_i\)

每一次计算 \(fa_i\) 容斥掉本身子树的贡献

我们还观察到, 我们少算本身贡献的东西了, 所以还需要额外的东西

\(g = \sum a_i\)

一个节点 \(fa_i\) 的贡献就是

\(ans = f_1(fa_i) - f_2(i) + dis(x, fa_i) \times (g(fa_i) - g(i))\)

我们需要找到一个点, 所以我们想到二分, 树上怎么二分? 点分树上二分!

用到它要求的性质 -- 唯一性

对于任意一个不是最优解的, 必然存在唯一一个它的儿子, 走到它的儿子更优

证明可以通过调整的插值来证明

所以我们可以二分

求解的东西和上边一样, 不再赘述

东西很好维护, 问题是 强制在线 直接暴力重构

这个题恶心的在于代码与细节 这是大模拟吧

边分治

对边进行分治

具体过程是

  1. 多叉树转二叉树
  2. 找中心边
  3. 处理经过中心边的路径
  4. 递归左右两个连通块

多叉树转二叉树是为了保证复杂度, 如果是一个菊花图的话就会很慢

这里给出一个封装的 多叉树转二叉树

namespace BT {
    int head[N], egtot;
    edge eg[N << 1];
    int last[N], tot;
    void add (int u, int v, int w) {
        eg[++egtot].to = v;
        eg[egtot].w = w;
        eg[egtot].nxt = head[u];
        head[u] = egtot;
    }
    void ins (int u, int v, int w) {
        ++tot;
        add(tot, v, w), add(v, tot, w);
        add(last[u], tot, 0), add(tot, last[u], 0);
        last[u] = tot;
    }
    void build (int x, int fa) {
        for (int i = head[x]; i; i = eg[i].nxt) {
            int to = eg[i].to;
            if (to == fa) continue;
            ins(x, to, eg[i].w);
            build(to, x);
        }
    }
    void _build_ () {
        for (int i = 1; i <= n; i++) last[i] = i;
        tot = n;
        build(1, 0);
    }
} 
posted @ 2025-02-16 19:33  d3genera7e  阅读(14)  评论(0)    收藏  举报