题解:洛谷 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
【核心思想】
-
问题分析:给定 \(N\) 个人和水桶,每次操作所有人同时将 \(i\) 单位水加入自己手中的每个水桶,然后将水桶递给 \(A_i\)。需要回答 \(Q\) 个查询:第 \(T_i\) 次操作后水桶 \(B_i\) 中的水量。这是一个倍增(Binary Lifting)问题,关键在于将 \(10^9\) 次操作转化为对功能图的快速路径求和,利用二进制分解将 \(O(T)\) 模拟优化为 \(O(\log T)\)。
-
算法选择:
- 功能图(Functional Graph):每个节点 \(i\) 有且仅有一条出边指向 \(A_i\),形成由若干基环树组成的结构
- 倍增预处理:\(f[u][i]\) 表示从节点 \(u\) 出发走 \(2^i\) 步到达的节点,\(v[u][i]\) 表示这段路径上经过的节点编号之和(即贡献的水量基数)
- 二进制分解:将操作次数 \(T\) 拆分为 \(2\) 的幂次之和,通过查表 \(O(\log T)\) 完成路径跳跃
-
关键步骤:
- 初始化(\(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\) 步后的位置
- 若 \(T\) 的第 \(i\) 位为 \(1\)(即 \(T \& (1 \ll i) \neq 0\)):
- 输出 \(ans\)
- 正确性说明:第 \(k\) 次操作时,持有某水桶的人编号为 \(p_k\),则该次操作向此桶加 \(p_k\) 单位水。\(T\) 次操作后总水量 = \(\sum_{k=1}^{T} p_k\),恰好等于路径上节点编号之和
- 初始化(\(2^0 = 1\) 步):
-
时间/空间复杂度:
- 时间复杂度:预处理 \(O(N \log T_{\max})\),单次查询 \(O(\log T_{\max})\),总查询 \(O(Q \log T_{\max})\)
- 空间复杂度:\(O(N \log T_{\max})\),两个倍增表 \(f\) 和 \(v\),维度为 \(N \times 30\)
-
倍增的核心思想:
- 二进制分解降维:任何整数 \(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
浙公网安备 33010602011771号