1 条题解

  • 0
    @ 2025-12-11 21:26:09

    题目分析

    目标: 给定一棵以 1 为根的完全二叉树(节点数 nn 很大),每个节点 ii 有初始数量 aia_i 和单价 bib_i。我们需要增加某些 aia_i,使得对于任意 x2kx \ge 2^k,满足路径和性质:

    $$\sum_{j=0}^k a_{\lfloor x/2^j \rfloor} \equiv 0 \pmod m $$

    即:xx 及其往上 kk 个祖先(共 k+1k+1 个节点)的 aa 值之和是 mm 的倍数。求最小花费。

    核心思路推导

    1. 发现周期性(同余类)

    这是解题的关键。记 S(x)=j=0kax/2jS(x) = \sum_{j=0}^k a_{\lfloor x/2^j \rfloor}。 根据题意,对于任意 x2kx \ge 2^k,都有 S(x)0(modm)S(x) \equiv 0 \pmod m

    考虑 xx 的孩子 2x2x(假设 2xn2x \le n),根据定义:

    $$S(2x) = a_{2x} + a_x + a_{\lfloor x/2 \rfloor} + \dots + a_{\lfloor x/2^{k-1} \rfloor} $$$$S(x) = \quad \quad a_x + a_{\lfloor x/2 \rfloor} + \dots + a_{\lfloor x/2^{k-1} \rfloor} + a_{\lfloor x/2^k \rfloor} $$

    观察两式,相减得:

    S(2x)S(x)=a2xax/2kS(2x) - S(x) = a_{2x} - a_{\lfloor x/2^k \rfloor}

    因为要求 S(2x)0S(2x) \equiv 0S(x)0(modm)S(x) \equiv 0 \pmod m,所以必须满足:

    a2xax/2k(modm)a_{2x} \equiv a_{\lfloor x/2^k \rfloor} \pmod m

    结论:在树上,任意节点 uu 和它的第 k+1k+1 代祖先 v=u/2k+1v = \lfloor u/2^{k+1} \rflooraa 值在模 mm 意义下必须同余。

    这意味着,我们不需要处理整棵深度为 logn\log n 的树,只需要处理顶部深度为 k+1k+1 的子树(约 2k+12^{k+1} 个节点)。整棵大树中所有深层的节点,都可以映射(“折叠”)到这前 k+1k+1 层对应的祖先上。

    2. 数据压缩与预处理

    代码中的 init 函数和 val 数组就是用来处理这种映射的。

    • 问题中的 kk vs 代码中的 kk: 题目中求和项数是 k+1k+1 个。代码中执行了 k++,所以代码里的 kk 代表层数(即题目中的 k+1k+1)。下文分析以代码中的 kk 为准。
    • 映射init 函数通过 fath[p >> k] 找到每个节点 pp 在顶部 kk 层对应的“代表节点” rtrtf[rt][rem]:记录了所有映射到 rtrt 的节点中,初始 aa 值模 mmremrem 的修改代价之和(即 bb 值之和)。
    • 预计算代价val[u][x]:表示将代表节点 uu(以及所有映射到 uu 的深层节点)的 aa 值模 mm 统一修改为 xx 所需的总代价。 计算方式:遍历所有可能的初始余数 rr,计算将 rr 增加到 xx(周期为 mm)所需的增量 disdis,乘以对应的总代价 f[u][r]f[u][r]

    3. 树形 DP

    经过预处理,问题转化为:在一棵只有 kk 层的满二叉树上,给每个节点 uu 确定一个值 vu[0,m1]v_u \in [0, m-1],使得从根到任意叶子的路径上,节点值之和 0(modm)\equiv 0 \pmod m,且总修改代价最小。

    • 状态定义dp[u][j]:以 uu 为根的子树中,满足所有子路径约束,且uu 到该子树叶子节点的路径和mmjj 时的最小代价。 注意:这里的路径和是指从当前节点 uu 向下累加到第 kk 层的叶子节点的和。

    • 状态转移: 对于节点 uu,枚举它自身选定的余数 dd(即 aud(modm)a_u \equiv d \pmod m)。 它的左右儿子 lc,rclc, rc 需要提供的路径和为 remrem。 那么 uu 的路径和 j=(rem+d)(modm)j = (rem + d) \pmod m。 反之,如果我们枚举 uu 的总路径和 jj 和子节点的路径和 remrem,则 uu 自身的值应为 (jrem+m)(modm)(j - rem + m) \pmod m

      代码中的转移方程(dfs 函数):

      $$dp[u][to] = \min_{from \in [0, m-1]} ( dp[lc][from] + dp[rc][from] + val[u][dist] ) $$

      其中 dist=(tofrom+m)(modm)dist = (to - from + m) \pmod m 是节点 uu 自身选择的值。

      • toto:当前节点 uu 向下的路径和。
      • fromfrom:子节点向下的路径和(左右孩子必须一致,才能保证 uu 加上它们后路径和都为 toto)。
    • 边界与答案

      • 边界(第 kk 层,即叶子):dp[p][to] = val[p][to]。因为叶子向下的路径和就是它自己的值。
      • 答案dp[1][0]。即根节点向下到叶子的路径和模 mm 为 0。

    代码细节注解

    // 核心变量
    // f[u][rem]: 映射到节点 u 的所有原节点中,a_i % m == rem 的总花费 b_i 之和
    // val[u][x]: 强制节点 u (及其同余类) 的值为 x 的总代价
    // dp[u][j]: DP 状态,u 向下路径和为 j 的最小代价
    
    void init(int p, int dep) {
        if (p > n) return;
        int rt;
        // 寻找前 k 层的代表节点
        // 代码中 k 已经加 1,代表周期长度
        if (dep <= k) rt = p;
        else rt = fath[p >> k]; // 这是一个很妙的位运算寻找第 k 代祖先的方法
        
        fath[p] = rt;
        // 累加代价
        f[rt][a[p].a % m] += a[p].b; 
        init(p << 1, dep + 1), init(p << 1 | 1, dep + 1);
    }
    
    void dfs(int p, int dep) {
        if (p > n) return;
        // 边界:第 k 层 (逻辑上的叶子)
        if (dep == k) { 
            for (int to = 0; to < m; to ++)
                dp[p][to] = val[p][to]; // 路径和即为自身的值
            return;
        }
    
        dfs(p << 1, dep + 1), dfs(p << 1 | 1, dep + 1);
    
        // 状态转移
        for (int to = 0; to < m; to ++) {
            dp[p][to] = inf;
            for (int from = 0; from < m; from ++) {
                // from 是子节点的路径和
                // to 是当前节点的路径和
                // dis = to - from 即为当前节点 p 应该取的值
                int dis = to - from;
                if (dis < 0) dis += m;
                
                // 代价 = 左儿子代价 + 右儿子代价 + 当前节点取 dis 的代价
                dp[p][to] = min(dp[p][to], dp[p << 1][from] + dp[p << 1 | 1][from] + val[p][dis]);
            }
        }
    }
    
    void creation() {
        // ... 清空与生成数据 ...
        k ++; // 将题目中的 k 转化为层数 (题目涉及 k+1 个点)
        init(1, 1); // 预处理同余类代价
        
        // 计算 val 数组
        for (int i = 1; i < (1 << k); i ++) {
            for (int x = 0; x < m; x ++) {
                val[i][x] = 0;
                for (int r = 0; r < m; r ++) {
                    int dis = x - r;
                    if (dis < 0) dis += m;
                    val[i][x] += f[i][r] * dis; // 累加将余数 r 变为 x 的代价
                }
            }
        }
    
        dfs(1, 1); // 树形 DP
        printf("%lld\n", dp[1][0]); // 输出路径和为 0 的最小代价
    }
    

    复杂度分析

    1. 预处理:遍历整棵树 O(N)O(N)
    2. DP 状态数:树的前 kk 层大约有 2k2^k 个节点。每个节点有 mm 个状态。
    3. DP 转移:每个状态需要枚举子节点的状态 fromfrom0m10 \dots m-1),复杂度 O(m)O(m)
    4. 总 DP 复杂度O(2km2)O(2^k \cdot m^2)

    给定数据范围 k10,m200k \le 10, m \le 200: $2^{10} \times 200^2 \approx 1024 \times 40000 \approx 4 \times 10^7$。 题目时间限制 10s,即使有 T=10T=10 组数据,总计算量在可接受范围内。

    总结

    这道题利用了路径求和约束在满二叉树上的周期性,将一棵巨大的树(N=107N=10^7)压缩成了深度仅为 kk 的小树(2112^{11}),然后利用树形 DP 结合同余性质求解。这是处理树上周期性约束的经典套路。

    • 1

    信息

    ID
    6144
    时间
    3000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    3
    已通过
    1
    上传者