UOJ33 [UR#2] 树上 GCD

2023-02-21,,

UOJ33 [UR#2] 树上 GCD

简要题意: 给定一棵有根树,对于每个 \(i \in [1,n)\),求出下式的值:

\[Ans[i] = \sum_{u<v} \gcd({\rm{dis}}(u,{\rm{LCA}}(u,v)),{\rm{dis}}(v,{\rm{LCA}}(u,v)))
\]

特别地,\(\gcd(x,0) = \gcd(0,x) = x \ (x \neq 0)\)。

数据规模: \(n \le 2 \times 10^5\)。

题解: 对于一类求 \(\sum[\gcd(x,y) = i]\) 的数量的题目,常用的解法是先求出 \(\sum[i \mid \gcd(x,y)]\),然后通过枚举 \(i\) 的倍数进行容斥,得到 \(\gcd\) 恰好为 \(i\) 的数量,其本质就是狄利克雷后缀差分(我瞎起的名字)。而 \(\gcd\) 为 \(i\) 的倍数的答案,容易知道就是 \(\sum [(i \mid x) \ \land \ (i \mid y)]\),这可以通过合并 \(x\) 和 \(y\) 所在的 \(cnt\) 数组轻易求得。

回到本题上。套路地,我们考虑在 \({\rm{LCA}}(u,v)\) 处计算出点对 \((u,v)\) 对答案的贡献。比较 trivial 的做法是维护 \(cnt[u][d]\) 表示 \(u\) 的子树中到 \(u\) 的距离为 \(d\) 的点的数量,容易通过启发式合并维护,使用类似长链剖分的方式分析一下发现是 \(O(n)\) 的。

接下来,考虑在合并链的过程中,通过 \(cnt\) 数组求出答案。令 \(len[u]\) 表示 \(u\) 的子树内距离 \(u\) 的最大值。假设我们合并 \(cnt[u]\) 以及 \(cnt[v]\),其中 \(len[u] \ge len[v]\),显然这两条链只会对 \(i \le len[v]\) 的答案作出贡献。因此,可以枚举 \(i \in [1,len[v]]\),此时的复杂度显然为 \(O(n)\)。再考虑如何计算出 \(\sum[(i \mid x) \ \land \ (i \mid y)]\),对于较短的链 \(v\),直接暴力做 \(O(n \ln n)\) 的狄利克雷后缀和,于是现在问题在于不能对长链 \(u\) 暴力计算。

仍然是套路,不妨考虑对 \(i\) 进行根号分治。显然 \(i > \sqrt n\) 的部分,暴力在 \(cnt[u]\) 上跳 \(i\) 的倍数,复杂度是可以接受的 \(O(n \sqrt n)\)。而对于 \(i \le \sqrt n\),由于数量较少,则考虑直接维护。只需对于每个 \(i\),分别计算一遍,维护出 \(siz[u]\) 表示 \(u\) 的子树中距离 \(u\) 为 \(i\) 的倍数的数量,可以通过 \(u\) 向 \(u\) 的 \(i\) 级祖先更新以快速地维护。显然这部分的复杂度也是 \(O(n \sqrt n)\) 的,完全可以接受。

最后考虑一种特殊情况,即对于 \(u\) 是 \(v\) 的祖先的部分,由于在启发式合并时可能会交换 \(cnt[u]\) 与 \(cnt[v]\),故这种情况的贡献不能在合并过程中统计。但是也很好维护,因为注意到所有深度 \(\ge i\) 的点(深度指到根的距离)都会对 \(Ans[i]\) 造成 \(1\) 的贡献,所以只需要在输出答案时加上即可。

综上,整个程序的复杂度为 \(O(n \sqrt n)\),期望得分 \(100\)。

代码:

using ll = long long;
const int N = 2e5 + 10;
// kfa[u] 为 u 的 i 级祖先
// cnt[u] 为 u 的子树内深度为 i 的倍数 -1 的数量
// siz[u] 为 u 的子树内深度恰好为 i 的倍数的数量
int n, blk, fa[N], kfa[N], dep[N], cnt[N], siz[N], num[N];
ll ans[N];
vector<int> vec[N]; // cnt[u][d] 反过来储存
signed main() {
read(n);
for (int i = 2; i <= n; ++i)
read(fa[i]), dep[i] = dep[fa[i]] + 1, ++num[dep[i]];
for (int i = n; i >= 1; --i)
num[i] += num[i + 1];
iota(kfa + 1, kfa + n + 1, 1);
blk = ceil(sqrt(n));
for (int i = 1; i <= blk; ++i) {
fill(cnt + 1, cnt + n + 1, 0);
fill(siz + 1, siz + n + 1, 0);
for (int j = n; j >= 2; --j) {
cnt[kfa[j]] += siz[j] + 1;
ans[i] += 1ll * siz[fa[j]] * cnt[j];
siz[fa[j]] += cnt[j];
}
for (int j = 1; j <= n; ++j)
kfa[j] = fa[kfa[j]];
}
for (int i = n; i >= 2; --i) {
vec[i].push_back(1);
if (vec[i].size() > vec[fa[i]].size())
vec[i].swap(vec[fa[i]]);
int sz = vec[i].size(), szf = vec[fa[i]].size();
for (int j = blk + 1; j <= sz; ++j) {
int cnt1 = 0, cnt2 = 0;
for (int k = j; k <= sz; k += j) cnt1 += vec[i][sz - k];
for (int k = j; k <= szf; k += j) cnt2 += vec[fa[i]][szf - k];
ans[j] += 1ll * cnt1 * cnt2;
}
for (int j = 1; j <= sz; ++j)
vec[fa[i]][szf - j] += vec[i][sz - j];
}
for (int i = n - 1; i >= 1; --i)
for (int j = i + i; j < n; j += i)
ans[i] -= ans[j];
for (int i = 1; i < n; ++i)
write(ans[i] + num[i]), putchar('\n');
return 0;
}

总结: 这样一道题,不论是计算 \(\gcd\) 时的容斥,还是在 \(\rm LCA\) 处启发式合并时计算贡献,又或是在求 \(i\) 的倍数的数量时进行的根号分治,实际上都是非常套路、非常“板”的东西,一步步推下来思路是很顺畅的。相信各位选手们在仔细分析过此题后,一定能够很快地得到正解吧!没办法,谁叫C让我写题解呢

UOJ33 [UR#2] 树上 GCD的相关教程结束。

《UOJ33 [UR#2] 树上 GCD.doc》

下载本文的Word格式文档,以方便收藏与打印。