卡常递推---题解 P4619 【[SDOI2018]旧试题】
Vocalise
·
·
题解
非常神仙的题目,三元环做法已经非常神仙,但是 @big_news 的做法让这题更加神仙。
这个做法大概没什么人提到吧。
首先有
d(ijk)=\sum_{x|i}\sum_{y|j}\sum_{z|k}[x\perp y][y\perp z][x\perp z]
非常显然有 d(ijk)=\sum_{x|ijk}1,分开 i,j,k 数会重复,于是我们让 ijk 相同的质因子在 x,y,z 中只出现一个。
P3327 [SDOI2015]约数个数和 中有介绍。
&\ \ \ \ \sum_{x=1}^a\sum_{y=1}^b\sum_{z=1}^cd(xyz) \\
&=\sum_{x=1}^a\sum_{y=1}^b\sum_{z=1}^c\sum_{i|x}\sum_{j|y}\sum_{k|z}[i\perp j][j\perp k][i\perp k] \\
&=\sum_{x=1}^a\sum_{y=1}^b\sum_{z=1}^c \lfloor\frac ax\rfloor \lfloor\frac by\rfloor \lfloor\frac cz\rfloor[x\perp y][y\perp z][x\perp z] \\
&=\sum_{x=1}^a\lfloor\frac ax\rfloor \sum_{y=1}^b\sum_{z=1}^c \lfloor\frac by\rfloor \lfloor\frac cz\rfloor[x\perp y]\sum_{d|y,d|z}\mu(d)[x\perp z] \\
&=\sum_{x=1}^a\lfloor\frac ax\rfloor\sum_{d=1}^{\min(b,c)}\mu(d)\sum_{y=1}^{b/d}\sum_{z=1}^{c/d}\lfloor\frac b{dy}\rfloor\lfloor\frac c{dz}\rfloor[x\perp dy][x\perp dz] \\
&=\sum_{x=1}^a\lfloor\frac ax\rfloor\sum_{d=1}^{\min(b,c)}\mu(d)[x\perp d]\sum_{y=1}^{b/d}\sum_{z=1}^{c/d}\lfloor\frac b{dy}\rfloor\lfloor\frac c{dz}\rfloor[x\perp y][x\perp z] \\
\end{aligned}
注意到我们在三个互质中只反演了一个。从这里开始就不同了。
后面式子的形式引出了两个函数的定义:
f(n,k)=\sum_{i=1}^n \lfloor\frac ni\rfloor[i\perp k]
g(n,k)=\sum_{i=1}^n\mu(i)[i\perp k]
则原式可以直接代入 f:
\sum_{x=1}^a \lfloor\frac ax\rfloor\sum_{d=1}^{\min(b,c)}\mu(d)[d\perp x]f(\lfloor\frac b{d} \rfloor,x)f(\lfloor\frac c{d} \rfloor,x)
则如果找出 f(n,k) 在整除点的值和 g(n,k) 在分块右端点前缀和的计算方法,式子后一部分可以整除分块计算。
忽略 $k$ 中平方因子,而 $p$ 是 $k$ 中一个质因子(同样参见链接),我们得到结论:
$$g(n,k)=g(\lfloor\frac np\rfloor,k)+g(n,\frac kp)$$
那我们尝试同样分析 $f(n,k)$。
$$\begin{aligned}
&\ \ \ \ \ f(n,k)=\sum_{i=1}^n\lfloor\frac ni\rfloor[i\perp k] \\
&=\sum_{i=1}^n \lfloor\frac ni\rfloor[i\perp\frac kp]-\sum_{i=1} ^n\lfloor\frac ni\rfloor[i\perp\frac kp][p|i] \\
&=f(n,\frac kp)-\sum_{i=1}^{n/p}\lfloor\frac n{ip}\rfloor[i\perp\frac kp] \\
&=f(n,\frac kp)-f(\lfloor\frac np \rfloor,\frac kp)
\end{aligned}$$
边界也显然。
$$f(0,k)=0,f(n,1)=\sum_{i=1}^n\lfloor\frac ni \rfloor=\sum_{i=1}^nd(i)$$
$$g(0,k)=0,g(n,1)=\sum_{i=1}^n\mu(d)$$
两者可以简单地线性筛出来。
---
$f(n,k),g(n,k)$ 的第一维对于一个 $n$,取遍 $b,c$ 的整除点有 $\sqrt n$ 个;
第二维对于一个 $k$,有取遍其素因子个,这个数低于 $\log k$。那 $[1,n]$ 中的无平方因子数有多少呢?最粗略的估计下,它有 $O(n)$ 个,因为即使有 $\log_2n$ 个 $2$ 作为素数进行乘积,也仅有 $2^{\log_2n}=n$ 个。
显然实际远低于 $O(n)$,但是下文仍写为 $O(n)$。
得到状态是 $O(n\sqrt n)$ 的,$O(1)$ 转移,加上分块,总复杂度是 $O(n\sqrt n)$ 的。
于是用 `unordered_map` 维护 $f,g$,枚举无平方因子的 $x$,分块时递归计算得到答案。
我们发现,根据 $k$ 的本质是一个素因子集合,对于该集合相同的 $x$,没有必要全部枚举。
记 $sv(x)$ 为 $x$ 因子去重得到的数,它可以线性筛得出。
我们统计 $sv(x)$ 相同的 $x$ 的 $\lfloor\dfrac ax\rfloor $ 之和,枚举存在的 $sv(x)$ 即可。
```cpp
#include <cstdio>
#include <iostream>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <unordered_map>
#define umap std::unordered_map
typedef long long ll;
const int V = 100001;
const int p = 1000000007;
inline int read() {
int x = 0, f = 1; char ch = getchar();
while(ch > '9' || ch < '0') { if(ch == '-') f = -1; ch = getchar(); }
do x = x * 10 + ch - 48, ch = getchar(); while(ch >= '0' && ch <= '9');
return x * f;
}
int df[V],P[V],tt; ll mu[V],d[V],sv[V];
void Seive() {
mu[1] = d[1] = sv[1] = 1;
for(int i = 2;i < V;i++) {
if(!df[i]) P[++tt] = i, mu[i] = -1, d[i] = 2, sv[i] = i;
for(int j = 1;j <= tt && i * P[j] < V;j++) {
int t = i * P[j]; df[t] = 1;
if(i % P[j]) mu[t] = p - mu[i], d[t] = 2 * d[i] % p, sv[t] = sv[i] * P[j];
else mu[t] = 0, d[t] = (2 * d[i] % p - d[i / P[j]] + p) % p, sv[t] = sv[i];
}
}
for(int i = 1;i < V;i++) mu[i] = (mu[i] + mu[i - 1]) % p;
for(int i = 1;i < V;i++) d[i] = (d[i] + d[i - 1]) % p;
return;
}
ll pd[V],pdt,cl[V];
umap <int,umap <int,ll> > Fmap,Gmap;
ll F(int n,int k,int s) {
if(!n) return 0;
else if(k == 1) return d[n];
else if(Fmap[n].count(k)) return Fmap[n][k];
return Fmap[n][k] = (F(n,k / pd[s],s + 1) - F(n / pd[s],k / pd[s],s + 1) + p) % p;
}
ll G(int n,int k,int s) {
if(!n) return 0;
else if(k == 1) return mu[n];
else if(Gmap[n].count(k)) return Gmap[n][k];
return Gmap[n][k] = (G(n / pd[s],k,s) + G(n,k / pd[s],s + 1)) % p;
}
int main() {
Seive();
int tc = read();
while(tc--) {
int a = read(), b = read(), c = read(); ll ans = 0;
if(b > c) std::swap(b,c); if(a > b) std::swap(a,b);
for(int i = 1;i <= a;i++) cl[i] = 0;
for(int i = 1;i <= a;i++) cl[sv[i]] += a / i;
for(int x = 1;x <= a;x++) if(cl[x]) {
ll res = 0; pdt = 0;
for(int i = 1;i <= tt && P[i] <= x;i++)
if(!(x % P[i])) pd[++pdt] = P[i];
for(int i = 1;i <= b && i <= c;i++) {
int j = std::min(b / (b / i),c / (c / i));
res = (res + F(b / i,x,1) * F(c / i,x,1) % p * (G(j,x,1) - G(i - 1,x,1) + p) % p) % p;
i = j;
}
ans = (ans + cl[x] % p * res % p) % p;
}
std::printf("%lld\n",ans);
}
return 0;
}
```
---
结果 $a=b=c=10000$ 就跑了 $10s$,$a=b=c=100000$ 的根本出不来。
瓶颈在对 $f(n,k),g(n,k)$ 的维护上。考虑去掉二维的 `unordered_map`。
然后似乎无法可想?$f,g$ 的第一维是取满的 $O(\sqrt n)$,仍然考虑第二维。
如果对质因子集合的访问是随机的,就像我们初步得到的从小到大枚举的方法,无法解决问题。
那如果是顺序插入删除呢?
显然我们需要的是一个按 Dfs 序遍历质因子的顺序维护的 $f,g$ 的值。
那么可以把状态简单地顺序分层了。具体地,对于质数序列 $p_1,p_2,\cdots p_s$ 和 $k=\prod p_i$,将 $f(n,k)$ 和 $g(n,k)$ 记录在第 $s$ 层,转移时根据 $s-1$ 层的状态。
对于每一个 $k$ 统计答案。
$s$ 根据上面的分析,是 $O(\log_2n)$ 的,空间上完全可以承受。
注意转移中 $f$ 和 $g$ 用到的第一维永远只有 $O(\sqrt n)$ 个点,如上所述,$f$ 用到了整除点,$g$ 用到了整除点 $-1$ 的点。
实际复杂度比 $O(n\sqrt n)$ 要小不少,$100000$ 的样例大约 $4s$。
```cpp
#include <cstdio>
#include <iostream>
#include <cstring>
#include <cmath>
#include <algorithm>
typedef long long ll;
const int V = 100001;
const int p = 1000000007;
inline int read() {
int x = 0, f = 1; char ch = getchar();
while(ch > '9' || ch < '0') { if(ch == '-') f = -1; ch = getchar(); }
do x = x * 10 + ch - 48, ch = getchar(); while(ch >= '0' && ch <= '9');
return x * f;
}
int df[V],P[V],t;
ll mu[V],sv[V],d[V],cl[V];
void Seive() {
mu[1] = sv[1] = d[1] = 1;
for(int i = 2;i < V;i++) {
if(!df[i]) P[++t] = i, mu[i] = -1, sv[i] = i, d[i] = 2;
for(int j = 1;j <= t && i * P[j] < V;j++) {
int t = i * P[j]; df[t] = 1;
if(i % P[j]) mu[t] = -mu[i], sv[t] = sv[i] * P[j], d[t] = 2 * d[i] % p;
else mu[t] = 0, sv[t] = sv[i], d[t] = (2 * d[i] % p - d[i / P[j]] + p) % p;
}
}
for(int i = 1;i < V;i++) mu[i] = (mu[i - 1] + mu[i]) % p;
for(int i = 1;i < V;i++) d[i] = (d[i - 1] + d[i]) % p;
}
int a,b,c; ll ans;
ll f[V][20], g[V][20];
void Dfs(ll x,int s,int k) {
ll res = 0;
for(int i = 1;i <= b && i <= c;i++) {
int j = std::min(b / (b / i),c / (c / i));
res = (res + (g[j][k] - g[i - 1][k] + p) % p * f[b / i][k] % p * f[c / i][k] % p) % p;
i = j;
}
ans = (ans + cl[x] * res % p) % p;
for(int r = s;r <= t && x * P[r] <= a;r++) {
for(int i = 1;i <= b && i <= c;i++) {
int j = std::min(b / (b / i),c / (c / i));
f[b / i][k + 1] = (f[b / i][k] - f[b / i / P[r]][k] + p) % p;
f[c / i][k + 1] = (f[c / i][k] - f[c / i / P[r]][k] + p) % p;
g[j][k + 1] = (g[j / P[r]][k + 1] + g[j][k]) % p;
i = j;
}
Dfs(x * P[r],r + 1,k + 1);
}
return;
}
int main() {
Seive();
int t = read();
while(t--) {
a = read(), b = read(), c = read();
for(int i = 1;i <= a;i++) cl[i] = 0;
for(int i = 1;i <= a;i++) cl[sv[i]] += a / i;
for(int i = 1;i <= b && i <= c;i++) {
int j = std::min(b / (b / i),c / (c / i));
f[b / i][1] = d[b / i], f[c / i][1] = d[c / i];
g[j][1] = mu[j];
i = j;
}
ans = 0, Dfs(1,1,1);
std::printf("%lld\n",ans);
}
return 0;
}
```