1 条题解
-
0
题目分析
目标: 给定一棵以 1 为根的完全二叉树(节点数 很大),每个节点 有初始数量 和单价 。我们需要增加某些 ,使得对于任意 ,满足路径和性质:
$$\sum_{j=0}^k a_{\lfloor x/2^j \rfloor} \equiv 0 \pmod m $$即: 及其往上 个祖先(共 个节点)的 值之和是 的倍数。求最小花费。
核心思路推导
1. 发现周期性(同余类)
这是解题的关键。记 。 根据题意,对于任意 ,都有 。
考虑 的孩子 (假设 ),根据定义:
$$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} $$观察两式,相减得:
因为要求 且 ,所以必须满足:
结论:在树上,任意节点 和它的第 代祖先 的 值在模 意义下必须同余。
这意味着,我们不需要处理整棵深度为 的树,只需要处理顶部深度为 的子树(约 个节点)。整棵大树中所有深层的节点,都可以映射(“折叠”)到这前 层对应的祖先上。
2. 数据压缩与预处理
代码中的
init函数和val数组就是用来处理这种映射的。- 问题中的 vs 代码中的 :
题目中求和项数是 个。代码中执行了
k++,所以代码里的 代表层数(即题目中的 )。下文分析以代码中的 为准。 - 映射:
init函数通过fath[p >> k]找到每个节点 在顶部 层对应的“代表节点” 。f[rt][rem]:记录了所有映射到 的节点中,初始 值模 为 的修改代价之和(即 值之和)。 - 预计算代价:
val[u][x]:表示将代表节点 (以及所有映射到 的深层节点)的 值模 统一修改为 所需的总代价。 计算方式:遍历所有可能的初始余数 ,计算将 增加到 (周期为 )所需的增量 ,乘以对应的总代价 。
3. 树形 DP
经过预处理,问题转化为:在一棵只有 层的满二叉树上,给每个节点 确定一个值 ,使得从根到任意叶子的路径上,节点值之和 ,且总修改代价最小。
-
状态定义:
dp[u][j]:以 为根的子树中,满足所有子路径约束,且从 到该子树叶子节点的路径和模 为 时的最小代价。 注意:这里的路径和是指从当前节点 向下累加到第 层的叶子节点的和。 -
状态转移: 对于节点 ,枚举它自身选定的余数 (即 )。 它的左右儿子 需要提供的路径和为 。 那么 的路径和 。 反之,如果我们枚举 的总路径和 和子节点的路径和 ,则 自身的值应为 。
代码中的转移方程(
$$dp[u][to] = \min_{from \in [0, m-1]} ( dp[lc][from] + dp[rc][from] + val[u][dist] ) $$dfs函数):其中 是节点 自身选择的值。
- :当前节点 向下的路径和。
- :子节点向下的路径和(左右孩子必须一致,才能保证 加上它们后路径和都为 )。
-
边界与答案:
- 边界(第 层,即叶子):
dp[p][to] = val[p][to]。因为叶子向下的路径和就是它自己的值。 - 答案:
dp[1][0]。即根节点向下到叶子的路径和模 为 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 的最小代价 }复杂度分析
- 预处理:遍历整棵树 。
- DP 状态数:树的前 层大约有 个节点。每个节点有 个状态。
- DP 转移:每个状态需要枚举子节点的状态 (),复杂度 。
- 总 DP 复杂度:。
给定数据范围 : $2^{10} \times 200^2 \approx 1024 \times 40000 \approx 4 \times 10^7$。 题目时间限制 10s,即使有 组数据,总计算量在可接受范围内。
总结
这道题利用了路径求和约束在满二叉树上的周期性,将一棵巨大的树()压缩成了深度仅为 的小树(),然后利用树形 DP 结合同余性质求解。这是处理树上周期性约束的经典套路。
- 问题中的 vs 代码中的 :
题目中求和项数是 个。代码中执行了
- 1
信息
- ID
- 6144
- 时间
- 3000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 3
- 已通过
- 1
- 上传者