LOJ6435 「PKUSC2018」星际穿越
LOJ6435 「PKUSC2018」星际穿越
题目大意
一张 \(n\) 个点的无向图,从第 \(i\) 个点向 \([l_i, i - 1]\) 中所有点连无向边,边的长度均为 \(1\)。记 \(\mathrm{dist}(i, j)\) 表示从点 \(i\) 到点 \(j\) 的最短路长度。\(q\) 次询问,每次给出 \(l, r, x\)(保证 \(l < r < x\)),求 \(\frac{1}{r - l + 1}\sum_{y = l}^{r}\mathrm{dist}(x, y)\)。用分数表示。
数据范围:\(1\leq n, q\leq 3\times 10^5\)。
本题题解
先考虑如何快速回答一个 \(\mathrm{dist}(x, y)\) (\(x > y\))。
如果 \(y\geq l_x\),答案显然是 \(1\),特判这种情况。
否则我们至少会走两步。考虑第一步:
- 向左可以走到 \([l_x, x - 1]\) 里所有点。
- 向右可以走到所有的 \(z\),满足 \(l_z\leq x < z\)。
考虑第二步:
- 向左可以走到 \(\min_{z\in[l_x, x -1]\text{ or } l_z \leq x < z}\{l_z\}\)。注意到这个值一定是 \(< x\) 的。所以我们把 \(x < l_z < z\) 的 \(z\) 算进去也不影响结果。即原式等于 \(\min_{z = l_x}^{n}\{l_z\}\)。换句话说,原问题等价于:第一步可以到达右边任何一个点。且有一个推论:此后再也不需要向右走。
于是我们完成了一个初步转化,从求 \(x\) 到 \(y\) 的距离,变成了求 \(l_x\) 及其右边所有点到 \(y\) 的距离的最小值。形式化地说:
设 \(t(i, j)\) 表示从 \(i\) 或 \(i\) 右边任意一个点出发,走 \(j\) 步,能到达的最左边的点(隐含之意是它右边的点都能在 \(j\) 步以内到达)。则:
- \(t(i, 1) = \min_{k = i}^{n}\{l_k\}\)
- \(t(i, j) = \min_{k = t(i, j - 1)}^{n}\{l_k\}\quad (j\geq 2)\)
可以发现一个事实:\(t(i, x + y) = t(t(i, x), y)\) (\(x, y\geq 1\))。结合实际意义非常好理解,也就是任何一种走 \(x + y\) 步的方案,总会经历先走 \(x\) 步,再走 \(y\) 步。
于是考虑倍增。设 \(f(i, j) = t(i, 2^{j})\),也就是从 \(i\) 或其右边任意一个点出发,走 \(2^{j}\) 步,能到达的最左边的点。那么:
- \(f(i, 0)=\min_{k = i}^{n}\{l_k\}\)
- \(f(i, j) = f(f(i, j - 1), j - 1)\quad (j\geq 1)\)
利用 \(f\) 数组,可以快速回答一个 \(\mathrm{dist}(x, y)\)。首先特判 \(y\geq l_x\)。然后转化为求 \(l_x\) 或其右边任意点,到 \(y\) 的最短路。做法:从 \(\lfloor\log_2(n)\rfloor\) 到 \(0\)(从大到小)枚举 \(2\) 的次幂 \(j\)。如果从当前的 \(x\) 走 \(2^j\) 步,到达的点 \(f(x, j) > y\),则走过去(\(x\gets f(x, j)\)),并令答案加上 \(2^{j}\)。最终得到的位置 \(x\) 一定距离 \(y\) 恰好为 \(1\)(类似于倍增法求 LCA 时,停在的位置恰好是 LCA 的儿子)。
但这只是求一对 \(\mathrm{dist}(x, y)\),题目要求 \(x\) 到一段区间的距离和。设 \(D(x, y)\) 表示 \(x\) 到 \([y, x - 1]\) 里所有点的距离和,则答案等于 \(D(x, l) - D(x, r + 1)\)。可以分别计算。
用一个数组记录从 \(i\) 或其右边任意点到 \([f(i, j), i - 1]\) 里所有点的距离和。即 \(s(i, j) = \sum_{k = f(i, j)}^{i - 1} \min_{u = i}^{n}\mathrm{dist}(u, k)\)。它的转移是:\(s(i, j) = s(i, j - 1) + s(f(i, j - 1), j - 1) + (f(i, j - 1)-f(i, j))\cdot 2^{j - 1}\)。
利用 \(s\) 数组,可以快速回答一个 \(D(x, y)\)。做法:首先特判 \(y\geq l_x\)。然后分 \([l_x, x - 1]\) 和 \([y, l_x)\) 里的点两类分别算。前者答案显然是 \(x - l_x\)。后者可以用上述的倍增法求出。
时间复杂度 \(\mathcal{O}((n + q)\log n)\)。
参考代码
// problem: LOJ6435
#include <bits/stdc++.h>
using namespace std;
#define mk make_pair
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
const int MAXN = 3e5;
const int LOG = 18;
ll gcd(ll x, ll y) { return (!y) ? x : gcd(y, x % y); }
int n, l[MAXN + 5];
int f[MAXN + 5][LOG + 1];
ll s[MAXN + 5][LOG + 1];
ll calc(int x, int y) {
assert(x >= y);
if (y >= l[x]) {
return x - y;
}
ll res = x - l[x];
x = l[x];
int dis = 1;
for (int j = LOG; j >= 0; --j) {
if (f[x][j] > y) {
res += (ll)dis * (x - f[x][j]) + s[x][j];
dis += (1 << j);
x = f[x][j];
}
}
res += (ll)(dis + 1) * (x - y);
return res;
}
int main() {
cin >> n;
multiset<int> minl;
for (int i = 2; i <= n; ++i) {
cin >> l[i];
minl.insert(l[i]);
}
for (int i = 2; i <= n; ++i) {
f[i][0] = *minl.begin(); // 右边最小的
s[i][0] = (i - f[i][0]);
minl.erase(minl.find(l[i]));
for (int j = 1; j <= LOG; ++j) {
f[i][j] = f[f[i][j - 1]][j - 1];
s[i][j] = s[i][j - 1] + s[f[i][j - 1]][j - 1] + (ll)(f[i][j - 1] - f[i][j]) * (1 << (j - 1));
}
}
int q; cin >> q;
for (int tq = 1; tq <= q; ++tq) {
int l, r, x;
cin >> l >> r >> x;
ll fz = calc(x, l) - calc(x, r + 1);
ll fm = r - l + 1;
ll g = gcd(fz, fm);
fz /= g;
fm /= g;
cout << fz << "/" << fm << endl;
}
return 0;
}