题目大意
小 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)
\]
观察题目中的操作规则,可以发现以下性质:
-
操作的本质:
- 每次操作 $ \frac{x + y}{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); // 从根节点开始 DFScout << 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 $,该算法可以轻松通过所有测试数据。