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

题目中的两种移动规则具有非常明确的唯一确定性:
- 向上移动:每个节点的父节点是唯一确定的(根节点指向自身);
- 向下移动:每个节点编号最小的子节点也是唯一确定的(叶节点指向自身)。
由于这两种移动都相当于在两条固定的“单向链”上前进,若单次一步步模拟移动,面对大步数时会超时。因此,我们可以预处理出每个节点向上和向下走 步所能到达的节点,建立两套倍增表。对于每次给定的移动步数 ,将其进行二进制拆分,即可在 时间内直接“跳”到目标节点。
- 从当前节点向上移动 步到其第 级祖先。
- 如果移动次数超过当前节点到根节点 的距离,则停留在根节点 。
- 可以使用经典的树上倍增表
fa[u][i](表示节点 的第 级祖先)在 时间内完成。
- 从当前节点向下移动,每次都选择子节点中编号最小的那个。
- 如果到达叶子节点,则停止移动。
- 由于每个节点 的“最小子节点”是唯一固定的(记为
min_child[u]),这构成了另一条向下的单向链。 - 同样可以使用倍增表
down[u][i](表示沿最小子节点向下走 步到达的节点)在 时间内完成。若中间到达叶子节点,倍增表记录为叶子节点本身即可。
-
预处理
min_child遍历树的所有节点,对每个非叶节点找到其所有子节点中编号最小的一个,记为
min_child[u]。如果 是叶节点,则min_child[u] = u。 -
构建倍增数组
向上倍增数组
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]。
- 初始
-
回答查询
对于每次旅行的序列 :
- 若 :利用
fa数组从当前节点向上跳 步。 - 若 :利用
down数组从当前节点向下跳 步。
- 若 :利用
#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;}- 时间复杂度: 预处理倍增表为 ;处理所有查询为 。总时间复杂度满足 的要求。
- 空间复杂度: ,空间限制为 ,完全够用。