给你一个以 1 为根的树,问你有对于每个 1~n-1 的 i,有多少个点对,它们分别到它们 LCA 的距离的 GCD 是 i。
树上GCD
题目大意
给你一个以 1 为根的树,问你有对于每个 1~n-1 的 i,有多少个点对,它们分别到它们 LCA 的距离的 GCD 是 i。
思路
吐槽:这是一道神(e)奇(xin)的题目,整个过程操作都非常的神(xuan)奇(xue)。
首先我们看到这些路径长度之类的,自然想到点分治。
但你会发现统计 GCD 不是很知道要怎么搞,而这里有一个方法,就是容斥。
首先你求出 GCD 是 x 的倍数的个数(这个其实好求,你只需要让两个距离都是 x 的倍数就行),然后我们进行容斥,倒序枚举数,然后再枚举它的倍数,那后面是都处理好的,就是 GCD 是 ix 的个数,那你要求 GCD 是 x 的,那就是用 GCD 是 x 倍数的减去 GCD 是 2x,3x,4x,... 的个数,然后这么 O(nlogn) 搞一遍,就好了。
那你考虑怎么搞之前,你会发现一个很特殊的东西,就是 gcd(0,x)=x,那这是什么情况呢?就是其中一个是 LCA,就是说在树上是一条链。
那你容易想到可以单独统计这种情况,先求出有多少个点到根节点的距离是 x,那容易想到以这些点为链的下面,链的上面可以在这个链的任意一个位置,那就会有 x,x−1,x−2,...,1 这么多种情况的距离。
那你就倒叙枚举后缀和加一下就行了。
那接着我们就来搞一个点分治的部分。
然后你会发现它是一个有根树,我们不能像在无根树上面那么乱搞。
那我们考虑搞有根树点分治,要怎么搞呢?我们继续分情况讨论。
假设你现在处理一个点,你要枚举它子树嘛,那如果你选的两个点都在原来树的子树,那就是像无根树那样搞。(第一种情况)
至于怎么搞,我们要四个数组 numi,sumi,Tnumi,Tsumi。
numi 是有多少个深度为 i 的点。(对于当前枚举的子树)Tnumi 则是对于你之前枚举过的子树。(这样防止算重,Tsumi 也一样)
sumi 是有多少个深度是 i 倍数的点。
这两个都可以跑一遍子树得到。
那容易想到,我们就是每次把 sumi×Tsumi 统计如 ansi 中,然后把它们加进 Tnumi,Tsumi 里。
但如果一个不是呢?也就是说,它在原来的树中是这个树的父亲。(第二种情况)
那你就直接暴力跳这些点,跳到你之前的当前子树根节点,找到它子树的点们,然后进行配对。
跟配对就是跟在原来树的所有子树中的点配对。
那前面我们把一开始求出的 Tnumi 保留,然后算出现在你跳到点的子树的 sumi,然后再配对。
为什么不能用之前的 Tsumi 呢?因为你现在跳到的点到根节点是有距离的,那就是说它一开始会间隔一段的距离,那就跟之前间隔 0 不同了。那你就要重新求一次。
那容易想到这个复杂度会裂开。
那你考虑记忆化,因为间隔的次数 x 顶多是你枚举的求 GCD 为 x 倍数的 x。
但容易看到 x 是 105 级别,会超空间。
那你考虑每次你枚举 x 之后,你后面加时每次加复杂度是 O(mdx)(md 是子树的最深深度),那你容易想到分块。
对于前 √n 我们记忆化,对于 >√n 我们就直接暴力求,后面这一部分求就是 O(md log md) 了,就可以接受了。
关于具体的实现可以看看代码。
代码
#include<queue>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#define INF 0x3f3f3f3f3f3f3f3f
#define ll long long
using namespace std;
struct node {
int to, nxt;
}e[400001];
int n, fa[200001], le[200001], KK, SUM;
int deg[200001], root, sz[200001], lstdeg;
int f[200001], num[200001], sum[200001];
int Tnum[200001], Tsum[200001], maxdeg;
int rem[1001][1001], UP, tmp, noww, lim;
ll ans1[200001], ans2[200001], tmpp;
bool in[200001];
queue <int> q;
int read() {
int re = 0;
char c = getchar();
while (c < '0' || c > '9') c = getchar();
while (c >= '0' && c <= '9') {
re = (re << 3) + (re << 1) + c - '0';
c = getchar();
}
return re;
}
void write(ll x) {
if (x > 9) write(x / 10);
putchar(x % 10 + '0');
}
void add(int x, int y) {
e[++KK] = (node){y, le[x]}; le[x] = KK;
e[++KK] = (node){x, le[y]}; le[y] = KK;
}
void work_line() {
for (int i = 1; i <= n; i++) {
deg[i] = deg[fa[i]] + 1;
ans1[deg[i] - 1]++;
}
}
void get_root(int now, int father) {
sz[now] = 1;
f[now] = 0;
for (int i = le[now]; i; i = e[i].nxt)
if (!in[e[i].to] && e[i].to != father) {
get_root(e[i].to, now);
sz[now] += sz[e[i].to];
f[now] = max(f[now], sz[e[i].to]);
}
f[now] = max(f[now], SUM - sz[now]);
if (f[now] < f[root]) root = now;
}
void get_road(int now, int father, int deg, int &maxdeg) {
maxdeg = max(maxdeg, deg);
num[deg]++;
for (int i = le[now]; i; i = e[i].nxt)
if (!in[e[i].to] && e[i].to != father)
get_road(e[i].to, now, deg + 1, maxdeg);
}
void get_case1(int now) {
for (int i = le[now]; i; i = e[i].nxt)
if (e[i].to != fa[now] && !in[e[i].to]) {
maxdeg = 0;
get_road(e[i].to, now, 1, maxdeg);
lstdeg = max(lstdeg, maxdeg);
for (int j = 1; j <= maxdeg; j++) {
for (int k = j; k <= maxdeg; k += j)
sum[j] += num[k];
ans2[j] += 1ll * sum[j] * Tsum[j];
Tsum[j] += sum[j]; Tnum[j] += num[j];
sum[j] = num[j] = 0;
}
}
}
void get_case2(int now, int father, int cnt) {
maxdeg = 0;
for (int i = le[now]; i; i = e[i].nxt)
if (e[i].to != father && !in[e[i].to] && e[i].to != fa[now]) {
get_road(e[i].to, now, 1, maxdeg);
lstdeg = max(lstdeg, maxdeg);
}
for (int i = 1; i <= maxdeg; i++)
for (int j = i; j <= maxdeg; j += i)
sum[i] += num[j];
lim = min(UP, maxdeg);
for (int i = 1; i <= lim; i++) {
if (rem[i][cnt % i] == -1) {
rem[i][cnt % i] = 0;
q.push(i); q.push(cnt % i);
for (int j = (i - (cnt % i)) % i; j <= lstdeg; j += i)
rem[i][cnt % i] += Tnum[j];
}
ans2[i] += 1ll * rem[i][cnt % i] * sum[i];
}
for (int i = lim + 1; i <= maxdeg; i++) {
tmpp = 0;
for (int j = (i - (cnt % i)) % i; j <= lstdeg; j += i)
tmpp += Tnum[j];
ans2[i] += 1ll * tmpp * sum[i];
}
for (int i = 1; i <= maxdeg; i++)
num[i] = sum[i] = 0;
}
void slove(int now) {
lstdeg = 0;
in[now] = 1;
get_case1(now);
Tnum[0] = 1;
tmp = 0;
noww = now;
while (fa[noww] && !in[fa[noww]]) {
get_case2(fa[noww], noww, ++tmp);
noww = fa[noww];
}
for (int i = 0; i <= lstdeg; i++)
Tnum[i] = Tsum[i] = 0;
while (!q.empty()) {
int a = q.front();
q.pop();
int b = q.front();
q.pop();
rem[a][b] = -1;
}
for (int i = le[now]; i; i = e[i].nxt)
if (!in[e[i].to]) {
root = 0;
SUM = sz[e[i].to];
get_root(e[i].to, 0);
slove(root);
}
}
void work_dfz() {
root = 0;
SUM = n;
get_root(1, 0);
slove(root);
}
void rongchi() {
for (int i = n - 1; i >= 1; i--)
ans1[i] += ans1[i + 1];
for (int i = n - 1; i >= 1; i--)
for (int j = i + i; j <= n - 1; j += i)
ans2[i] -= ans2[j];
}
int main() {
n = read();
for (int i = 2; i <= n; i++) {
fa[i] = read();
add(fa[i], i);
}
work_line();
f[0] = INF;
UP = 1000;
memset(rem, -1, sizeof(rem));
work_dfz();
rongchi();
for (int i = 1; i <= n - 1; i++) {
write(ans1[i] + ans2[i]);
putchar('\n');
}
return 0;
}
__EOF__
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现