P13019 [GESP202506 八级] 树上旅行

https://www.luogu.com.cn/problem/P13019

💡算法总体思路

  • \(f[i][k]\) :从节点 \(i\) **向上跳 \(2^k\) ** 步后到达的节点编号。
  • \(g[i][k]\) :从节点 \(i\) **沿最小编号子链向下跳 \(2^k\) ** 步后到达的节点编号。
  • \(h\) :指针变量,用来在 fg 之间切换(正数向上走用 f,负数向下走用 g)。
  • 输入序列每个 \(a_j\)
    • 若为正 → 向上跳 \(a_j\)
    • 若为负 → 向下跳 \(-a_j\)

倍增表 实现快速跳跃,每次跳 \(O(\log n)\)

📘详细逐行注释版代码

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

const int N = 100010;  // 最大节点数 n <= 1e5
const int M = 17;      // log2(1e5) ≈ 17,倍增层数

int n, m;              // n = 节点数, m = 查询数
int f[N][M];           // f[i][k] = 从 i 向上跳 2^k 步后的节点
int g[N][M];           // g[i][k] = 从 i 沿最小子节点链向下跳 2^k 步后的节点
int (*h)[M];           // h 是一个“二维数组指针”,可指向 f 或 g(即在二者间切换)

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);

    cin >> n >> m;

    // ----------- 建树输入与最小子节点初始化 -----------
    for (int i = 2; i <= n; i ++ )
    {
        int p;          // p 是节点 i 的父亲
        cin >> p;
        f[i][0] = p;    // 倍增表第一层:父节点

        // 更新父节点 p 的最小编号子节点(g[p][0])
        // 若还没记录过,或已有子节点编号更大,则更新为当前 i
        if (!g[p][0] || g[p][0] > i)
            g[p][0] = i;
    }

    // 根节点的父亲定义为自己
    f[1][0] = 1;

    // 若某节点没有子节点,则令它的最小子节点为自己(方便倍增)
    for (int i = 1; i <= n; i ++ )
        if (!g[i][0])
            g[i][0] = i;

    // ----------- 倍增预处理 -----------
    // f[i][k] = f[f[i][k-1]][k-1]
    // g[i][k] = g[g[i][k-1]][k-1]
    // 即从 i 连跳 2^(k-1) + 2^(k-1) 步的结果
    for (int k = 1; k < M; k ++ )
        for (int i = 1; i <= n; i ++ )
        {
            f[i][k] = f[f[i][k - 1]][k - 1];
            g[i][k] = g[g[i][k - 1]][k - 1];
        }

    // ----------- 处理每个旅行查询 -----------
    while (m -- )
    {
        int s, cnt;   // s = 起点编号, cnt = 移动序列长度
        cin >> s >> cnt;

        // 对每个移动 a_j 按顺序执行
        while (cnt -- )
        {
            int k;
            cin >> k;

            // 若 k > 0 表示“向上跳 k 次”,则 h 指向 f;
            // 若 k < 0 表示“向下跳 |k| 次”,则 h 指向 g。
            if (k > 0)
                h = f;
            else
                h = g, k = -k;  // 转为正数方便处理

            // 倍增跳跃:
            // 遍历每一位,如果二进制第 i 位是 1,就跳 2^i 步。
            for (int i = 0; i < M; i ++ )
                if (k >> i & 1)
                    s = h[s][i];
        }

        cout << s << endl;  // 输出终点编号
    }

    return 0;
}

🧠逻辑总结与要点说明

操作 含义 数据结构 特殊处理
向上跳 从当前结点走向父亲 f[i][k] 根的父亲设为自己(防止越界)
向下跳 从当前结点走到“编号最小的子节点” g[i][k] 若无子节点则设为自己
h 当前操作方向(上或下) 指向 fg 用函数指针形式节省代码
倍增跳 用二进制快速跳多步 O(log n) 检查每个二进制位

⏱️复杂度分析

阶段 时间复杂度
建树与预处理 \(O(n \log n)\)
每次查询 \(O(k_i \log n)\) ,其中 \(k_i\) 是本次旅行移动数
总计 \(O((n + \sum k_i)\log n)\) ,满足题目限制
posted @ 2025-10-24 13:19  katago  阅读(2)  评论(0)    收藏  举报