题解:洛谷 AT_abc438_e Heavy Buckets

【题目来源】

洛谷:AT_abc438_e [ABC438E] Heavy Buckets - 洛谷

【题目描述】

\(N\) 人和 \(N\) 桶。人和水桶的编号都是 \(1, 2, \ldots, N\)

最初,人 \(i\) 只持有水桶 \(i\) ,而水桶 \(i\) 是空的。

从现在起,将执行以下操作 \(10^9\) 次:

  • 同时在 \(i = 1, 2, \ldots, N\) 中,\(i\)\(i\) 个单位的水添加到自己手中的每个水桶中,并将这些水桶递给 \(A_i\)

在这里,水桶中的水量没有限制。

对于 \(i = 1, 2, \ldots, Q\) ,请回答下列问题:

  • 求第 \(T_i\) 次操作后,水桶 \(B_i\) 中的水量。

【输入】

输入内容由标准输入法提供,格式如下

\(N\) \(Q\)
\(A_1\) \(A_2\) \(\ldots\) \(A_N\)
\(T_1\) \(B_1\)
\(T_2\) \(B_2\)
\(\vdots\)
\(T_Q\) \(B_Q\)

【输出】

输出 \(Q\) 行。第 \(i\) 行应包含第 \(i\) 次查询的答案。

【输入样例】

5 6
3 4 2 2 5
4 3
6 5
1 4
10 1
10 2
1000000000 1

【输出样例】

11
30
4
28
30
2999999998

【核心思想】

  1. 问题分析:给定 \(N\) 个人和水桶,每次操作所有人同时将 \(i\) 单位水加入自己手中的每个水桶,然后将水桶递给 \(A_i\)。需要回答 \(Q\) 个查询:第 \(T_i\) 次操作后水桶 \(B_i\) 中的水量。这是一个倍增(Binary Lifting)问题,关键在于将 \(10^9\) 次操作转化为对功能图的快速路径求和,利用二进制分解将 \(O(T)\) 模拟优化为 \(O(\log T)\)

  2. 算法选择

    • 功能图(Functional Graph):每个节点 \(i\) 有且仅有一条出边指向 \(A_i\),形成由若干基环树组成的结构
    • 倍增预处理\(f[u][i]\) 表示从节点 \(u\) 出发走 \(2^i\) 步到达的节点,\(v[u][i]\) 表示这段路径上经过的节点编号之和(即贡献的水量基数)
    • 二进制分解:将操作次数 \(T\) 拆分为 \(2\) 的幂次之和,通过查表 \(O(\log T)\) 完成路径跳跃
  3. 关键步骤

    • 初始化\(2^0 = 1\) 步):
      • \(f[u][0] = A_u\):从 \(u\)\(1\) 步到达 \(A_u\)
      • \(v[u][0] = u\):第 \(1\) 步操作时,\(u\) 向手中水桶加 \(u\) 单位水,贡献为 \(u\)
    • 倍增预处理\(i\)\(1\)\(29\),因为 \(2^{30} > 10^9\)):
      • \(f[u][i] = f[f[u][i-1]][i-1]\):从 \(u\)\(2^i\) 步 = 先走 \(2^{i-1}\) 步,再走 \(2^{i-1}\)
      • \(v[u][i] = v[u][i-1] + v[f[u][i-1]][i-1]\):路径和 = 前半段和 + 后半段和
    • 查询处理(对于每个查询 \((T, B)\)):
      • 初始化 \(ans = 0\),当前位置 \(b = B\)
      • 从高位到低位遍历 \(i\)\(29\)\(0\)):
        • \(T\) 的第 \(i\) 位为 \(1\)(即 \(T \& (1 \ll i) \neq 0\)):
          • \(ans \mathrel{+}= v[b][i]\):累加走 \(2^i\) 步经过的节点编号之和
          • \(b = f[b][i]\):跳跃到 \(2^i\) 步后的位置
      • 输出 \(ans\)
    • 正确性说明:第 \(k\) 次操作时,持有某水桶的人编号为 \(p_k\),则该次操作向此桶加 \(p_k\) 单位水。\(T\) 次操作后总水量 = \(\sum_{k=1}^{T} p_k\),恰好等于路径上节点编号之和
  4. 时间/空间复杂度

    • 时间复杂度:预处理 \(O(N \log T_{\max})\),单次查询 \(O(\log T_{\max})\),总查询 \(O(Q \log T_{\max})\)
    • 空间复杂度:\(O(N \log T_{\max})\),两个倍增表 \(f\)\(v\),维度为 \(N \times 30\)
  5. 倍增的核心思想

    • 二进制分解降维:任何整数 \(T\) 可唯一表示为若干 \(2^i\) 之和,将线性遍历转化为对数级跳跃
    • 倍增递推\(f[u][i]\)\(f[u][i-1]\)\(f[f[u][i-1]][i-1]\) 组合得到,体现"大事化小、小事化了"的分治思想
    • 路径信息可叠加性\(v[u][i]\) 满足可加性,保证二进制分解后各段的和能直接累加得到总答案
    • 离线预处理、在线查询:先 \(O(N \log T)\) 建表,后 \(O(\log T)\) 回答每个查询,适用于大量重复查询场景
    • 适用于"功能图上的路径查询"、"多次大跨度跳跃"、"区间信息可合并"类问题

【算法标签】

普及 #倍增

【代码详解】

#include <bits/stdc++.h>
using namespace std;
#define int long long  // 定义int为long long,处理大数

const int N = 200005;
int n, Q;  // n: 数组长度,Q: 查询次数
int a[N];  // 原始数组

// 倍增表,第一维是节点,第二维是2的幂次
// f[u][i]: 从节点u开始,走2^i步到达的节点
// v[u][i]: 从节点u开始,走2^i步经过的节点编号之和
int f[N][30], v[N][30];

signed main()  // 因为#define int long long,所以用signed main
{
    // 输入数据
    cin >> n >> Q;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
        // 初始化:走2^0=1步
        f[i][0] = a[i];  // 从i走1步到a[i]
        v[i][0] = i;     // 经过的节点是起始节点i本身
    }

    // 预处理倍增表
    for (int i = 1; i < 30; i++)  // 2^30 > 1e9,足够大
    {
        for (int u = 1; u <= n; u++)  // 遍历所有节点
        {
            // 从u走2^i步 = 从u走2^(i-1)步,再走2^(i-1)步
            f[u][i] = f[f[u][i-1]][i-1];

            // 路径和 = 前2^(i-1)步的和 + 后2^(i-1)步的和
            v[u][i] = v[u][i-1] + v[f[u][i-1]][i-1];
        }
    }

    // 处理查询
    while (Q--)
    {
        int t, b;  // t: 走的步数,b: 起始节点
        cin >> t >> b;

        int ans = 0;  // 存储路径上节点编号之和

        // 二进制分解t,从高位到低位处理
        for (int i = 29; i >= 0; i--)
        {
            // 如果t的第i位是1
            if (t & (1 << i))
            {
                // 累加从b开始走2^i步经过的节点编号之和
                ans += v[b][i];
                // 更新当前位置到走2^i步后的位置
                b = f[b][i];
            }
        }

        cout << ans << endl;
    }

    return 0;
}

【运行结果】

5 6
3 4 2 2 5
4 3
11
6 5
30
1 4
4
10 1
28
10 2
30
1000000000 1
2999999998
posted @ 2026-08-27 11:04  团爸讲算法  阅读(7)  评论(0)    收藏  举报