2025“钉耙编程”中国大学生算法设计春季联赛(4)1004 充实

题目大意

小 hua 用一棵树模型描述自己的竞赛生涯,树上的每条从根到叶子的路径代表一个“支线”。每个节点有一个点权 $ a_i $,且越往下的节点点权越大(构成小根堆)。对于每条“支线”,我们需要判断它是否是充实的。具体来说:

  • 将路径上的点权放入集合 $ S $。
  • 每次可以从 $ S $ 中选择两个奇偶性相同的数 $ x, y $,并将 $ \frac{x + y}{2} $ 放入 $ S $,重复此操作若干次。
  • 如果最终 $ S $ 能够覆盖从 $ \min(S) $ 到 $ \max(S) $ 的完整值域,则该路径是充实的。

给定树的结构和点权,求有多少条从根到叶子的路径是充实的。


解题思路

核心思想:差分数组与最大公约数 (GCD)

通过分析题目条件,可以将问题转化为对路径上点权的差分数组进行性质判断。以下是解题的关键步骤:


1. 差分数组的性质

对于一条路径上的点权序列 $ a_1, a_2, \dots, a_k $,我们定义其差分数组为:

\[d_i = a_{i+1} - a_i \quad (i = 1, 2, \dots, k-1) \]

观察题目中的操作规则,可以发现以下性质:

  1. 操作的本质

    • 每次操作 $ \frac{x + y}{2} $ 实际上是对差分数组中某些元素进行约简。
    • 具体来说,如果差分数组中有偶数,可以通过不断除以 2 进行约简。
  2. 合法路径的充要条件

    • 差分数组的所有元素的最大公约数 $ \gcd(d_1, d_2, \dots, d_{k-1}) $ 必须是 2 的幂次形式(即 $ \gcd(d_1, d_2, \dots, d_{k-1}) = 2^t $,其中 $ t \geq 0 $)。

因此,判断一条路径是否充实,只需计算其差分数组的 GCD 是否满足上述条件。


2. DFS 过程中动态维护 GCD

为了高效地统计所有路径的结果,我们可以使用深度优先搜索 (DFS) 在树上遍历:

  • 从根节点开始,沿着每条路径递归向下。
  • 在递归过程中,动态维护当前路径的差分数组的 GCD。
  • 当到达叶子节点时,检查当前路径的 GCD 是否为 2 的幂次形式,若是则计入答案。

这种方法的时间复杂度为 $ O(n \log a) $,其中 $ n $ 是树的节点数,$ \log a $ 是计算 GCD 的复杂度。


3. 特殊边界处理

在实现过程中需要注意以下细节:

  • 树的根节点只有一个点权 $ a_1 $,没有差分值。
  • 对于单节点路径(即只有一个点),直接判定为不充实。
  • GCD 的初始值设置为 0,表示尚未开始计算。

代码实现

以下是完整的代码实现:

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 1e5 + 10;

void solve() {
    int n; cin >> n;
    vector<vector<int>> G(n + 1); // 邻接表存储树
    for (int i = 2; i <= n; i++) {
        int fa; cin >> fa;
        G[fa].push_back(i);
    }
    vector<int> a(n + 1); // 点权
    for (int i = 1; i <= n; i++) cin >> a[i];

    int cnt = 0; // 记录充实路径的数量

    // 定义 DFS 函数
    function<void(int, int)> dfs = [&](int u, int gc) {
        if (G[u].empty()) { // 叶子节点
            if (gc == 0 || (gc & (gc - 1)) == 0) cnt++; // 判断 gcd 是否为 2 的幂次
            return;
        }
        for (auto v : G[u]) {
            dfs(v, __gcd(gc, a[v] - a[u])); // 动态维护 GCD
        }
    };

    dfs(1, 0); // 从根节点开始 DFS
    cout << cnt << '\n';
}

signed main() {
    ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
    int T; cin >> T;
    while (T--) solve();
}

代码解析

1. 输入与建图

  • 使用邻接表 G 存储树的结构。
  • 输入点权数组 a,并保证 $ a_{f_i} \leq a_i $。

2. DFS 函数

  • 参数说明:
    • u:当前节点编号。
    • gc:当前路径的差分数组的 GCD。
  • 递归过程:
    • 如果当前节点是叶子节点,检查 GCD 是否为 2 的幂次形式。
    • 否则,递归访问子节点,并更新 GCD。

3. 边界条件

  • 单节点路径直接判定为不充实。
  • GCD 初始值为 0,表示尚未开始计算。

时间复杂度

算法的时间复杂度为:

\[O(T \cdot n \cdot \log a) \]

其中:

  • $ T $ 是测试数据组数。
  • $ n $ 是树的节点数。
  • $ \log a $ 是计算 GCD 的复杂度。

对于 $ n \leq 10^5 $ 和 $ a \leq 10^9 $,该算法可以轻松通过所有测试数据。


posted @ 2025-03-29 03:31  archer2333  阅读(215)  评论(0)    收藏  举报