P6788 「EZEC-3」四月樱花 解题报告


P6788 「EZEC-3」四月樱花 解题报告

题意分析

这道题给出了一个非常复杂的数学公式来计算樱花飘落的数量 \(s\),我们需要计算出 \(s\) 对一个大质数 \(p\) 取模后的结果。

\[s=\prod_{x=1}^t\prod_{y|x}\frac{y^{d(y)}}{\prod_{z|y}(z+1)^2} \]

公式里有连乘(\(\prod\))、整除(\(y|x\))、约数个数(\(d(y)\))等多种运算,而且 \(t\) 的最大值达到了 \(2.5 \times 10^9\),这意味着我们不可能通过直接循环计算来求解,必须对这个公式进行化简,找到一个高效的算法。

Step 1: 化繁为简 —— 公式变形

面对这么复杂的公式,我们的第一步永远是尝试化简。

关键点一:处理 \(y^{d(y)}\)

\(d(y)\) 代表 \(y\) 的约数个数。根据定义,\(d(y) = \sum_{z|y} 1\)
那么,\(y^{d(y)}\) 就可以写成:

\[y^{d(y)} = y^{\sum_{z|y} 1} = \prod_{z|y} y \]

举个例子:\(y=6\),它的约数有 \(1, 2, 3, 6\),共 \(d(6)=4\) 个。那么 \(6^{d(6)} = 6^4 = 6 \times 6 \times 6 \times 6\)。这个式子可以看作是“对 6 的每一个约数,我们都乘上一个 6”。

关键点二:一个巧妙的代换

题解中用了一个非常精彩的技巧。对于任意一个数 \(y\) 和它的约数 \(z\),我们知道 \(y/z\) 也一定是 \(y\) 的一个约数。当 \(z\) 遍历 \(y\) 的所有约数时,\(y/z\) 也会不重不漏地遍历 \(y\) 的所有约数。

例如,对于 \(y=12\),它的约数集合是 \(\{1, 2, 3, 4, 6, 12\}\)
\(z\) 取这些值时,\(12/z\) 对应的值是 \(\{12, 6, 4, 3, 2, 1\}\),还是原来的集合!

利用这个性质,我们可以把 \(\prod_{z|y} y\) 分解:

\[\prod_{z|y} y = \prod_{z|y} (z \cdot \frac{y}{z}) = \left(\prod_{z|y} z\right) \cdot \left(\prod_{z|y} \frac{y}{z}\right) \]

因为 \(z\)\(y/z\) 遍历的是同一组数,所以 \(\prod_{z|y} \frac{y}{z} = \prod_{z|y} z\)
因此,

\[\prod_{z|y} y = \left(\prod_{z|y} z\right) \cdot \left(\prod_{z|y} z\right) = \left(\prod_{z|y} z\right)^2 = \prod_{z|y} z^2 \]

化简结果

将这个结果代回原公式中 \(y^{d(y)}\) 的位置,我们得到:

\[\frac{y^{d(y)}}{\prod_{z|y}(z+1)^2} = \frac{\prod_{z|y}z^2}{\prod_{z|y}(z+1)^2} = \prod_{z|y}\frac{z^2}{(z+1)^2} \]

现在,原公式变成了:

\[s = \prod_{x=1}^t\prod_{y|x}\prod_{z|y}\frac{z^2}{(z+1)^2} \]

关键点三:改变枚举顺序

这个三层嵌套的连乘依然很复杂。我们换个角度思考:对于一个固定的 \(z\),它到底在最终的乘积里贡献了多少次?

一个包含 \(z\) 的项 \(\frac{z^2}{(z+1)^2}\) 会被乘入答案,当且仅当存在 \(x\)\(y\) 满足 \(1 \le x \le t, y|x, z|y\)
这个条件链 z | y | x 等价于 z | x

所以,我们只需要计算对于一个固定的 \(z\),有多少对 \((x,y)\) 满足 \(1 \le x \le t, z|y, y|x\)
这个计数过程等价于计算 \(\sum_{k=1}^{\lfloor t/z \rfloor} \lfloor \frac{\lfloor t/z \rfloor}{k} \rfloor\)
我们把这个函数记为 \(f(n) = \sum_{k=1}^{n} \lfloor \frac{n}{k} \rfloor\)
那么,对于每个 \(z\),它对应的底数 \(\frac{z^2}{(z+1)^2}\) 的指数就是 \(f(\lfloor t/z \rfloor)\)

最终,公式被化简为:

\[s = \prod_{z=1}^t \left(\frac{z^2}{(z+1)^2}\right)^{f\left(\lfloor\frac{t}{z}\rfloor\right)} \]

Step 2: 核心算法 —— 整除分块

观察化简后的公式,我们发现指数部分是 \(f(\lfloor\frac{t}{z}\rfloor)\)
熟悉数论的同学会立刻认出 \(\lfloor\frac{t}{z}\rfloor\) 的形式——这是整除分块的标志!

整除分块告诉我们,当 \(z\) 在某个连续的区间 \([l, r]\) 内变化时,\(\lfloor\frac{t}{z}\rfloor\) 的值是恒定的。
这意味着,对于同一个块 \([l, r]\) 内的所有 \(z\),它们的指数 \(f(\lfloor\frac{t}{z}\rfloor)\) 都是相同的!

设在区间 \([l, r]\) 内,\(\lfloor\frac{t}{z}\rfloor = V\),则指数为 \(f(V)\)
这个区间对答案的贡献就是:

\[\prod_{z=l}^r \left(\frac{z^2}{(z+1)^2}\right)^{f(V)} = \left(\prod_{z=l}^r \frac{z^2}{(z+1)^2}\right)^{f(V)} \]

底数部分是一个裂项连乘

\[\prod_{z=l}^r \frac{z^2}{(z+1)^2} = \frac{l^2}{(l+1)^2} \times \frac{(l+1)^2}{(l+2)^2} \times \dots \times \frac{r^2}{(r+1)^2} = \frac{l^2}{(r+1)^2} \]

中间项都约掉了!这个底数可以用快速幂和逆元在 \(O(\log p)\) 时间内算出。

我们的主算法框架就有了:

  1. 使用整除分块,遍历 \(z\)\(1\)\(t\) 的所有块 \([l, r]\)
  2. 对于每个块,计算出共同的指数 \(E = f(\lfloor t/l \rfloor)\)
  3. 计算出这个块的底数 \(B = \frac{l^2}{(r+1)^2} \pmod p\)
  4. \(B^E \pmod p\) 乘入最终答案。

现在的问题是,如何快速计算 \(f(n) = \sum_{k=1}^{n} \lfloor \frac{n}{k} \rfloor\)
巧了,这个函数本身也可以用整除分块在 \(O(\sqrt{n})\) 的时间内计算。
这样,我们就有了一个“整除分块套整除分块”的算法,总时间复杂度大约是 \(O(t^{3/4})\),对于本题的数据范围还是太慢。

Step 3: 极致优化 —— 预处理与平衡复杂度

\(O(t^{3/4})\) 的瓶颈在于每次都要重新计算 \(f(V)\)。我们可以像杜教筛等算法一样,通过预处理来优化。

我们再次审视 \(f(n)\)

\[f(n) = \sum_{i=1}^n \lfloor\frac{n}{i}\rfloor = \sum_{i=1}^n \sum_{j=1, i|j}^n 1 = \sum_{j=1}^n \sum_{i|j} 1 = \sum_{j=1}^n d(j) \]

这里的 \(d(j)\) 还是约数个数。这个推导说明,\(f(n)\) 其实就是 \(d(1)\)\(d(n)\) 的前缀和!

我们可以预处理出一部分 \(f(n)\) 的值。

  1. 预处理 \(d(n)\):我们可以用类似筛法的方式,在 \(O(M \log M)\) 的时间内计算出 \(1\)\(M\) 所有数的约数个数。
    // M是预处理的范围
    for (int i = 1; i <= M; ++i) {
        for (int j = i; j <= M; j += i) {
            d[j]++; // i是j的约数
        }
    }
    
  2. 预处理 \(f(n)\):对 \(d(n)\) 数组求一遍前缀和,就能得到 \(f(n)\)
    f[0] = 0;
    for (int i = 1; i <= M; ++i) {
        f[i] = f[i-1] + d[i];
    }
    

这样,我们就得到了一个混合策略

  • 设定一个阈值 \(M\)(比如 \(10^6\))。
  • \(O(M \log M)\) 的时间内预处理出 \(1\)\(M\) 的所有 \(f(n)\) 值。
  • 在主算法的整除分块中,当需要计算 \(f(V)\) 时:
    • 如果 \(V \le M\),直接从预处理的数组中 \(O(1)\) 读取。
    • 如果 \(V > M\),再用 \(O(\sqrt{V})\) 的整除分块方法在线计算。

这种“预处理+在线计算”的平衡策略,可以将总时间复杂度优化到 \(O(t^{2/3})\) 级别,足以通过本题。

代码实现与注意事项

  1. 数据范围\(t\) 最大为 \(2.5 \times 10^9\),超过了 int 的范围(约 \(2.1 \times 10^9\)),需要使用 unsigned intlong long
  2. 模数:所有乘法运算都要对 \(p\) 取模。计算 \(a/b \pmod p\) 时,需要使用费马小定理求 \(b\) 的逆元,即 \(a \times b^{p-2} \pmod p\)
  3. 指数的模数:这是本题最关键的细节!根据费马小定理 \(a^{p-1} \equiv 1 \pmod p\),我们在计算一个数的幂时,指数需要对 \(\varphi(p)=p-1\) 取模。所以,在计算 \(f(V)\) 的值以及它的累加过程时,所有的加法和乘法都应该对 p-1 取模。
  4. 最终实现:
    • 写一个 preprocess 函数,预处理出 \(M\)(例如 \(10^6\))以内的 \(f\) 函数值(注意对 p-1 取模)。
    • 写一个 calc_f 函数,根据参数大小选择查表或在线计算。
    • 主函数中进行整除分块,循环变量 lr 使用 unsigned int
    • 在循环中,调用 calc_f 计算指数,用快速幂计算底数的幂,并累乘到答案中。

总结

本题是一道综合性很强的数论题,从看似复杂的公式出发,通过一系列精妙的数学推导和变换,最终化简为一个可以用整除分块解决的问题。为了满足时间要求,我们进一步运用了类似杜教筛的预处理思想,平衡了预处理和在线计算的开销,将复杂度降至可通过的范围。

解题的关键路径是:
公式化简 \(\rightarrow\) 发现整除分块结构 \(\rightarrow\) 优化子问题计算(预处理)\(\rightarrow\) 注意模数细节

这道题很好地锻炼了我们的数论推导能力和对常用算法(如整除分块)的深层理解与优化能力。
附录:
好的,完全没问题!这部分推导确实是整个解题思路中最具技巧性的一步,我们一步一步把它拆解清楚。

我们的目标是理解这个公式转换:

\[s = \prod_{x=1}^t\prod_{y|x}\prod_{z'|y}\frac{(z')^2}{(z'+1)^2} \quad \xrightarrow{? \quad} \quad s = \prod_{z=1}^t \left(\frac{z^2}{(z+1)^2}\right)^{f\left(\lfloor\frac{t}{z}\rfloor\right)} \]

(我把最内层的变量写成 \(z'\) 以免和我们后面要分析的固定的 \(z\) 混淆)

核心思想:改变视角,从“贡献”出发

原来的公式是“遍历-计算”模式:

  1. 选一个 x 从 1 到 t
  2. 对于这个 x,选一个它的约数 y
  3. 对于这个 y,选一个它的约数 z'
  4. \(\frac{(z')^2}{(z'+1)^2}\) 乘到总结果里。

这个过程非常繁琐。我们换一个角度,采用“贡献法”:
我们不按 x, y, z' 的顺序算,而是直接问:对于一个特定的、固定的数字 z(比如 z=2),它的对应项 \(\frac{2^2}{(2+1)^2}\) 最终在总乘积中出现了多少次?

这个出现的次数,就是它在最终公式里的指数

Step 1: 找到一个 z 出现的条件

我们看原始公式的循环条件:

\[\prod_{x=1}^t \quad \prod_{y|x} \quad \prod_{z'|y} \]

对于一个我们选定的 z,它的项 \(\frac{z^2}{(z+1)^2}\) 会被乘进去一次,当且仅当存在一组 (x, y) 满足:

  1. x1t 之间 (\(1 \le x \le t\))。
  2. yx 的约数 (\(y|x\))。
  3. zy 的约数 (\(z|y\))。

所以,z 的指数 = 满足以上三个条件的 (x, y) 数对的个数。

Step 2: 简化条件,开始计数

我们来分析这三个条件:\(1 \le x \le t, \quad z|y, \quad y|x\)
这是一个“整除链”:\(z\) 整除 \(y\),同时 \(y\) 整除 \(x\)。根据整除的传递性,这必然意味着 \(z\) 整除 \(x\)

所以,问题被转化成:
对于一个固定的 z,我们要数出有多少对 (x, y) 满足:

  1. \(1 \le x \le t\)
  2. \(z | x\)
  3. \(z | y\)
  4. \(y | x\)

现在我们来正式计数。我们不能直接数 xy,因为它们相互关联。我们可以通过变量代换来解耦。

既然 \(z|x\)\(z|y\),那么 \(x\)\(y\) 肯定都是 \(z\) 的倍数。我们可以设:

  • \(x = k \cdot z\)
  • \(y = m \cdot z\)

其中 \(k\)\(m\) 都是正整数。现在我们把原来的条件用 \(k\)\(m\) 来重写:

  1. \(1 \le x \le t \implies 1 \le k \cdot z \le t \implies 1 \le k \le \lfloor \frac{t}{z} \rfloor\)
  2. \(y|x \implies (m \cdot z) | (k \cdot z) \implies m|k\)

现在条件变得清爽多了!对于一个固定的 z,它的指数等于满足以下新条件的 (k, m) 数对的个数:

  1. \(1 \le k \le \lfloor \frac{t}{z} \rfloor\)
  2. \(m | k\) (并且 \(m \ge 1\)

Step 3: 计算数对 (k, m) 的数量

我们来计算满足新条件的 (k, m) 数对有多少个。我们可以枚举 k,然后计算每个 k 对应的 m 有多少个。

  • \(k=1\) 时,m 必须整除 1,所以 m=1。有 1 个 m
  • \(k=2\) 时,m 必须整除 2,所以 m=1, 2。有 2 个 m
  • \(k=3\) 时,m 必须整除 3,所以 m=1, 3。有 2 个 m
  • ...
  • 对于一个给定的 k,满足 m|km 的个数,正好就是 k约数个数,记为 \(d(k)\)

k 的取值范围是 \(1 \le k \le \lfloor \frac{t}{z} \rfloor\)。所以,总的数对个数就是把所有可能的 k 对应的 m 的个数加起来:

\[\text{总数对个数} = \sum_{k=1}^{\lfloor t/z \rfloor} d(k) \]

我们已经成功了一大半!我们证明了,对于每个 z,它的项 \(\frac{z^2}{(z+1)^2}\) 的指数是 \(\sum_{k=1}^{\lfloor t/z \rfloor} d(k)\)

Step 4: 连接到函数 f(n)

题解中定义了一个函数 \(f(n) = \sum_{i=1}^n \lfloor \frac{n}{i} \rfloor\)
而我们推导出的指数是 \(\sum_{k=1}^{\lfloor t/z \rfloor} d(k)\)
只要我们能证明 \(\sum_{k=1}^N d(k) = \sum_{i=1}^N \lfloor \frac{N}{i} \rfloor\) 对于任意正整数 \(N\) 都成立,那么我们的推导就和题解对接上了。

证明:\(\sum_{k=1}^N d(k) = \sum_{i=1}^N \lfloor \frac{N}{i} \rfloor\)

这个等式是一个非常经典和重要的数论恒等式。我们再次使用“贡献法”来证明。

看左边:\(\sum_{k=1}^N d(k)\)
根据定义,\(d(k) = \sum_{i|k, 1 \le i \le k} 1\)
所以左边等于 \(\sum_{k=1}^N \left( \sum_{i|k, 1 \le i \le N} 1 \right)\)

这是一个双重求和。我们再次交换求和顺序。

  • 原来的顺序是:先枚举 k 从 1 到 N,再枚举 k 的约数 i
  • 我们换成:先枚举 i 从 1 到 N,再看 i 是哪些 k 的约数。

对于一个固定的 i\(1 \le i \le N\)),它会作为哪些 k\(1 \le k \le N\))的约数出现呢?
ik 的约数,等价于 ki 的倍数。
所以,我们要找在 1N 之间有多少个数是 i 的倍数。
这些数是:\(1 \cdot i, 2 \cdot i, 3 \cdot i, \dots, j \cdot i, \dots\)
其中最大的倍数不能超过 N,即 \(j \cdot i \le N \implies j \le \lfloor \frac{N}{i} \rfloor\)
所以,k 的可能取值有 \(\lfloor \frac{N}{i} \rfloor\) 个。

这意味着,对于每个 i,它在 \(\sum_{k=1}^N d(k)\) 这个总和中被加了 \(\lfloor \frac{N}{i} \rfloor\) 次。
因此,总和就是把所有 i 的贡献加起来:

\[\sum_{i=1}^N \lfloor \frac{N}{i} \rfloor \]

证明完毕!

总结

我们把所有步骤串起来:

  1. 我们想求 z 的指数,也就是满足 \(1 \le x \le t, z|y, y|x\)(x, y) 数对数量。
  2. 通过变量代换 \(x=kz, y=mz\),问题转化为求满足 \(1 \le k \le \lfloor t/z \rfloor, m|k\)(k, m) 数对数量。
  3. 这个数量等于 \(\sum_{k=1}^{\lfloor t/z \rfloor} d(k)\)
  4. 我们证明了 \(\sum_{k=1}^N d(k) = \sum_{i=1}^N \lfloor \frac{N}{i} \rfloor = f(N)\)
  5. 所以 z 的指数就是 \(f(\lfloor t/z \rfloor)\)

最终,我们把原始公式中对 xy 的复杂遍历,成功转化为了对 z 的一次遍历,每个 z 的项 \(\frac{z^2}{(z+1)^2}\) 都带有一个指数 \(f(\lfloor t/z \rfloor)\)

\[s = \prod_{z=1}^t \left(\frac{z^2}{(z+1)^2}\right)^{f\left(\lfloor\frac{t}{z}\rfloor\right)} \]

这个推导就完成了。希望这次的详细步骤能帮助你理解!

posted @ 2025-07-20 16:47  surprise_ying  阅读(9)  评论(0)    收藏  举报