✏️ 纠错
第 138 题 / 共 155 题

题目描述

给定一棵有 n 个结点的 有根树,结点依次以 1,2,…,n 编号,其中根结点的编号为 1。

小 A 计划在这棵有根树上进行 q 次旅行。在第 i 次旅行中,小 A 首先选定结点 si​ 作为起点,并移动若干次。移动分为以下两种:

  1. 移动至当前结点的父结点。特殊地,如果当前位于根结点,则不进行移动。
  2. 移动至当前结点的所有子结点中编号最小的结点。特殊地,如果当前位于叶子结点,则不进行移动。

由于移动次数可能很大,对于第 i 次旅行,旅行中的移动以 ki​ 个不为零的整数构成的序列 ai,1​,ai,2​,…,ai,ki​​ 表示。对 ai,j​,若 ai,j​>0 则代表进行 ai,j​ 次第一种移动;若 ai,j​<0 则代表进行 −ai,j​ 次第二种移动。根据给出的序列从左至右完成所有移动后,小 A 所在的结点即是旅行的终点

给定每次旅行的起点与移动序列,请你求出旅行终点的结点编号。

输入格式

第一行,两个正整数 n,q,分别表示有根树的结点数量,以及旅行次数。

第二行,n−1 个整数 p2​,p3​,…,pn​,其中 pi​ 表示结点 i 的父结点编号。

接下来 2q 行中的第 2i−1 行(1≤i≤q)包含两个正整数 si​,ki​,分别表示第 i 次旅行的起点编号,以及移动序列的长度。第 2i 行包含 ki​ 个整数 ai,1​,ai,2​,…,ai,ki​​,表示移动序列。

输出格式

输出共 q 行,第 i 行包含一个整数,表示第 i 次旅行终点的结点编号。

输入输出样例

输入 #1

5 4
1 1 2 2
3 3
1 -1 -1
2 5
1 -1 1 -1 1
5 8
1 1 1 -1 -1 -1 -1 -1
5 3
-1 -1 1

输出 #1

4
1
4
2

输入 #2

8 3
5 4 2 1 3 6 6
8 1
8
8 2
8 -8
8 3
8 -8 8

输出 #2

1
7
1

说明/提示

子任务编号 测试点占比 n q ∑ki​ 特殊性质
1 20% ≤100 ≤100 ≤1000 保证 ai,j​ 为 1 或 −1
2 20% ≤104 ≤104 ≤4×104 仅包含第一种移动
3 20% ≤104 ≤104 ≤4×104 仅包含第二种移动
4 40% ≤105 ≤2×104 ≤105 -

对于所有测试点,保证:

  • 1≤n≤105
  • 1≤q≤2×104
  • 1≤pi​≤n
  • 1≤si​≤n
  • ki​≥1 且 ∑ki​≤105
  • 1≤∣ai,j​∣≤n
📝 题目解析

题目大意:
给定一棵有根树(根为1号点),有 q 次查询。每次从某个起点 s 出发,按照指令 ai,j 移动:ai,j>0:向上走 ai,j 步(即走到 ai,j 级祖先)。ai,j<0:向下走 ai,j 步,每一步都选择当前点所有子节点中编号最小的那个继续走。输出每次操作后停在的最终结点编号。
考纲知识点:倍增LCA
解析:
将题目转化一下,操作1就是从一个点向上跳x次,操作2就是从一个点向下跳x次。观察到x比较大,如果直接一步一步跳显然会超时。这时我们可以选择倍增。首先预处理出从一个点开始向上和向下跳2k步后会跳到哪里,然后用类似快速幂的方式跳,将x二进制分解,如果当前位是0就不跳,如果当前位是1就跳。时间复杂度O(klogn)。

写代码时注意一下边界情况的处理。

参考程序
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

const int N = 1e5 + 5;
const int LOG = 20;  // 足够处理1e5规模的树

int n, q;
int fa[N][LOG];  // 向上跳2^j步到达的节点
int ch[N][LOG];  // 向下跳2^j步到达的节点(每次跳向最小子节点)

// 向上跳x步
int get_up(int s, int x) {
    for (int j = 0; j < LOG; ++j) {
        if (x & (1 << j)) {
            s = fa[s][j];
        }
    }
    return s;
}

// 向下跳x步(每次跳向最小子节点)
int get_down(int s, int x) {
    for (int j = 0; j < LOG; ++j) {
        if (x & (1 << j)) {
            s = ch[s][j];
        }
    }
    return s;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    cin >> n >> q;
    
    // 初始化:每个节点的子节点先设为自身(表示无法向下跳)
    for (int i = 1; i <= n; ++i) {
        for (int j = 0; j < LOG; ++j) {
            ch[i][j] = i;
        }
    }
    
    // 根节点的父节点设为自身(表示无法向上跳)
    for (int j = 0; j < LOG; ++j) {
        fa[1][j] = 1;
    }
    
    // 存储每个节点的子节点,用于后续排序找到最小子节点
    vector<int> children[N];
    
    // 读取父节点信息
    for (int i = 2; i <= n; ++i) {
        int p;
        cin >> p;
        fa[i][0] = p;  // 向上跳1步(2^0)到达父节点
        children[p].push_back(i);
    }
    
    // 对每个节点的子节点排序,找到编号最小的子节点
    for (int i = 1; i <= n; ++i) {
        if (!children[i].empty()) {
            sort(children[i].begin(), children[i].end());
            ch[i][0] = children[i][0];  // 向下跳1步(2^0)到达最小子节点
        }
    }
    
    // 预处理倍增表
    for (int j = 1; j < LOG; ++j) {
        for (int i = 1; i <= n; ++i) {
            fa[i][j] = fa[fa[i][j-1]][j-1];  // 向上跳2^j步 = 先跳2^(j-1)步,再跳2^(j-1)步
            ch[i][j] = ch[ch[i][j-1]][j-1];  // 向下跳2^j步 = 先跳2^(j-1)步,再跳2^(j-1)步
        }
    }
    
    // 处理每次查询
    while (q--) {
        int s, k;
        cin >> s >> k;
        
        for (int i = 0; i < k; ++i) {
            int a;
            cin >> a;
            
            if (a > 0) {
                // 向上移动a次
                s = get_up(s, a);
            } else {
                // 向下移动|a|次
                s = get_down(s, -a);
            }
        }
        
        cout << s << '\n';
    }
    
    return 0;
}