跳过并跳转到主要内容
BigO

树上旅行

求在一棵 n 个节点的有根树(根节点为 1)上处理 q 次移动查询。移动规则分为两种:向上移动至父节点(若在根节点则原地不动)和向下移动至编号最小的子节点(若在叶节点则原地不动)。每次查询给出起点和一个移动序列,需要依次执行序列中的所有步数,并求出最终到达的节点编号。

GESP1分钟阅读

题目中的两种移动规则具有非常明确的唯一确定性:

  1. 向上移动:每个节点的父节点是唯一确定的(根节点指向自身);
  2. 向下移动:每个节点编号最小的子节点也是唯一确定的(叶节点指向自身)。

由于这两种移动都相当于在两条固定的“单向链”上前进,若单次一步步模拟移动,面对大步数时会超时。因此,我们可以预处理出每个节点向上和向下走 2i2^i 步所能到达的节点,建立两套倍增表。对于每次给定的移动步数 ai,ja_{i,j},将其进行二进制拆分,即可在 O(log⁡n)\mathcal{O}(\log n) 时间内直接“跳”到目标节点。

  • 从当前节点向上移动 xx 步到其第 xx 级祖先。
  • 如果移动次数超过当前节点到根节点 11 的距离,则停留在根节点 11。
  • 可以使用经典的树上倍增表 fa[u][i](表示节点 uu 的第 2i2^i 级祖先)在 O(log⁡n)\mathcal{O}(\log n) 时间内完成。

  • 从当前节点向下移动,每次都选择子节点中编号最小的那个。
  • 如果到达叶子节点,则停止移动。
  • 由于每个节点 uu 的“最小子节点”是唯一固定的(记为 min_child[u]),这构成了另一条向下的单向链。
  • 同样可以使用倍增表 down[u][i](表示沿最小子节点向下走 2i2^i 步到达的节点)在 O(log⁡n)\mathcal{O}(\log n) 时间内完成。若中间到达叶子节点,倍增表记录为叶子节点本身即可。

  1. 预处理 min_child

    遍历树的所有节点,对每个非叶节点找到其所有子节点中编号最小的一个,记为 min_child[u]。如果 uu 是叶节点,则 min_child[u] = u。

  2. 构建倍增数组

    向上倍增数组 fa[u][i]:

    • 初始 fa[u][0] = p[u],fa[1][0] = 1。
    • 转移方程:fa[u][i] = fa[fa[u][i-1]][i-1]。

    向下倍增数组 down[u][i]:

    • 初始 down[u][0] = min_child[u]。
    • 转移方程:down[u][i] = down[down[u][i-1]][i-1]。
  3. 回答查询

    对于每次旅行的序列 aia_i:

    • 若 ai,j>0a_{i,j} > 0:利用 fa 数组从当前节点向上跳 ai,ja_{i,j} 步。
    • 若 ai,j<0a_{i,j} < 0:利用 down 数组从当前节点向下跳 −ai,j-a_{i,j} 步。

#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>
using namespace std;
const int MAXN = 100005;
const int LOGN = 18; // log2(100005) < 18
int p[MAXN];
int min_child[MAXN];
int fa[MAXN][LOGN];
int down[MAXN][LOGN];
int main() {
// 优化 I/O 性能
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n, q;
if (!(cin >> n >> q)) return 0;
// 初始化 min_child 数组(默认为 0,表示没有子节点)
for (int i = 1; i <= n; ++i) {
min_child[i] = 0;
}
// 读取父节点,并预处理每个节点的“最小子节点”
for (int i = 2; i <= n; ++i) {
cin >> p[i];
int parent = p[i];
if (min_child[parent] == 0 || i < min_child[parent]) {
min_child[parent] = i;
}
}
// 预处理叶子节点和根节点的边缘情况
p[1] = 1; // 根节点 1 的父节点指向自己,实现停留在根节点的效果
for (int i = 1; i <= n; ++i) {
if (min_child[i] == 0) {
min_child[i] = i; // 叶子节点没有子节点,指向自己,实现停留在叶节点的效果
}
}
// 构建 2^0 步的倍增基底
for (int i = 1; i <= n; ++i) {
fa[i][0] = p[i];
down[i][0] = min_child[i];
}
// 构建倍增表 fa 和 down
for (int j = 1; j < LOGN; ++j) {
for (int i = 1; i <= n; ++i) {
fa[i][j] = fa[fa[i][j - 1]][j - 1];
down[i][j] = down[down[i][j - 1]][j - 1];
}
}
// 处理 q 次旅行查询
while (q--) {
int u, k;
cin >> u >> k;
for (int i = 0; i < k; ++i) {
int a;
cin >> a;
if (a > 0) {
// 向上跳 a 步
for (int j = LOGN - 1; j >= 0; --j) {
if ((a >> j) & 1) {
u = fa[u][j];
}
}
} else if (a < 0) {
// 向下跳 -a 步
int steps = -a;
for (int j = LOGN - 1; j >= 0; --j) {
if ((steps >> j) & 1) {
u = down[u][j];
}
}
}
}
cout << u << "\n";
}
return 0;
}

  • 时间复杂度: 预处理倍增表为 O(nlog⁡n)\mathcal{O}(n \log n);处理所有查询为 O((∑ki)log⁡n)\mathcal{O}((\sum k_i) \log n)。总时间复杂度满足 1.00s1.00\text{s} 的要求。
  • 空间复杂度: O(nlog⁡n)\mathcal{O}(n \log n),空间限制为 512MB512\text{MB},完全够用。

© 2026 三七开 · 一方通行,只管向前。

旅途由 Astro 驱动 · 主题 Chirping Astro