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的更多相关文章

  1. UOJ33 [UR #2] 树上GCD 【点分治】【容斥原理】【分块】

    题目分析: 树上点对问题首先想到点分治.假设我们进行了点分治并递归地解决了子问题.现在我们合并问题. 我们需要找到所有经过当前重心$ c $的子树路径.第一种情况是LCA为当前重心$ c $.考虑以$ ...

  2. 【uoj33】 UR #2—树上GCD

    http://uoj.ac/problem/33 (题目链接) 题意 给出一棵${n}$个节点的有根树,${f_{u,v}=gcd(dis(u,lca(u,v)),dis(v,lca(u,v)))}$ ...

  3. [UOJ UR #2]树上GCD

    来自FallDream的博客,未经允许,请勿转载,谢谢. 传送门 看完题目,一般人都能想到 容斥稳了 .这样我们只要统计有多少点对满足gcd是i的倍数. 考虑长链剖分,每次合并的时候,假设我已经求出轻 ...

  4. 【UOJ#33】【UR#2】树上GCD 有根树点分治 + 容斥原理 + 分块

    #33. [UR #2]树上GCD 有一棵$n$个结点的有根树$T$.结点编号为$1…n$,其中根结点为$1$. 树上每条边的长度为$1$.我们用$d(x,y)$表示结点$x,y$在树上的距离,$LC ...

  5. 【UOJ#33】【UR #2】树上GCD(长链剖分,分块)

    [UOJ#33][UR #2]树上GCD(长链剖分,分块) 题面 UOJ 题解 首先不求恰好,改为求\(i\)的倍数的个数,最后容斥一下就可以解决了. 那么我们考虑枚举一个\(LCA\)位置,在其两棵 ...

  6. uoj33 【UR #2】树上GCD

    题目 大致是长剖+\(\rm dsu\ on\ tree\)的思想 先做一个转化,改为对于\(i\in[1,n-1]\)求出有多少个\(f(u,v)\)满足\(i|f(u,v)\),这样我们最后再做一 ...

  7. [UOJ]#33. 【UR #2】树上GCD

    题目大意:给定一棵有根树,边长均为1,对于每一个i,求树上有多少个点对,他们到lca距离的gcd是i.(n<=200,000) 做法:先容斥,求出gcd是i的倍数的点对,考虑长链剖分后从小到大合 ...

  8. 【UR #2】树上GCD

    这道题是有根树点分治+烧脑的容斥+神奇的分块 因为是规定1为根,还要求LCA,所以我们不能像在无根树上那样随便浪了,必须规定父亲,并作特殊讨论 因为gcd并不好求,所以我们用容斥转化一下,求x为gcd ...

  9. UOJ#33. 【UR #2】树上GCD 点分治 莫比乌斯反演

    原文链接https://www.cnblogs.com/zhouzhendong/p/UOJ33.html 题解 首先我们把问题转化成处理一个数组 ans ,其中 ans[i] 表示 d(u,a) 和 ...

随机推荐

  1. Go | 讲解GOROOT、GOPATH、GOBIN

    前言 Go(又称 Golang)是 Google 开发的一种静态强类型.编译型.并发型,并具有垃圾回收功能的编程语言.Go 被誉为是未来的服务器端编程语言. Go是一门全新的静态类型开发语言,具有自动 ...

  2. etcd实现分布式锁

    转载自:etcd实现分布式锁 当并发的访问共享资源的时候,如果没有加锁的话,无法保证共享资源安全性和正确性.这个时候就需要用到锁 1.需要具备的特性 需要保证互斥访问(分布式环境需要保证不同节点.不同 ...

  3. 从0搭建vue3组件库: 如何完整搭建一个前端脚手架?

    相信大家在前端开发中都使用过很多前端脚手架,如vue-cli,create-vite,create-vue等:本篇文章将会为大家详细介绍这些前端脚手架是如何实现的,并且从零实现一个create-kit ...

  4. 走进shell

    走进shell 在Linux早起,还没有出现图形化,超哥和其他系统管理员都只能坐在电脑前,输入shell命令,查看控制台的文本输出. 在大多数Linux发行版里,例如centos,可以简单的用组合键来 ...

  5. 介绍一个jmeter录制脚本谷歌插件 —— metersphere-chrome-plugin

    该插件可将用户在浏览器操作时的 HTTP 请求记录下来并生成 JMX 文件(JMeter 脚本文件). 1. 插件解压 插件下载链接: https://pan.baidu.com/s/14nGb_s9 ...

  6. ubuntu生成pem证书连接服务器(已验证)

    SSH 密钥认证是什么? 与用户密码登录相比,SSH 密钥认证更安全,因为只有拥有密钥的人才能连接,并且密钥通过不同的算法进行了很好的加密.它还通过无密码登录使 SSH 连接变得简单. 这个搞两个方案 ...

  7. DTSE Tech Talk | 第10期:云会议带你入门音视频世界

    摘要:本期直播主题是<云会议带你入门音视频世界>,华为云媒体服务产品部资深专家金云飞,与开发者们交流华为云会议在实时音视频行业中的集成应用,帮助开发者更好的理解华为云会议及其开放能力. 本 ...

  8. 状态机的技术选型,yyds!

    前言 今天跟大家分享一个关于"状态机"的话题.状态属性在我们的现实生活中无处不在.比如电商场景会有一系列的订单状态(待支付.待发货.已发货.超时.关闭):员工提交请假申请会有申请状 ...

  9. c语言内存四区、数据存储范围和内存存储方向

    (1)代码区通常是共享只读(代码无法修改)的,即可以被其他的程序调用,例如运行两个qq,除了数据不一样,代码都是一样的, 每次运行qq,都会将代码和数据加载到内存中,除了数据,每次加载的代码都是一样的 ...

  10. C++实现真值表

    这一片文章主要是关于真值表,在完成之前我也遇到了许多问题.比如怎么去求解表达式的值,怎么去将每个变量进行赋值,也就是如何 将n个字符进行01全排列. 01全排列真的神奇,01全排列其实就是2^n.他可 ...