数论分块
计算这个式子的和:\(S = \sum \limits_{i=1}^n \left\lfloor \dfrac{n}{i} \right\rfloor\)。比如,当 \(n=10\) 时,需要计算 \(\left\lfloor \dfrac{10}{1} \right\rfloor + \left\lfloor \dfrac{10}{2} \right\rfloor + \left\lfloor \dfrac{10}{3} \right\rfloor + \cdots + \left\lfloor \dfrac{10}{10} \right\rfloor = 10 + 5 + 3 + 2 + 2 + 1 + 1 + 1 + 1 + 1\)。
如果 \(n\) 很小,写一个 for 循环从 \(1\) 到 \(n\) 暴力计算当然没问题。但如果 \(n\) 非常大,\(O(n)\) 的循环就会超时。需要一个更快的方法。
仔细观察每一个值,可以发现,\(\left\lfloor \dfrac{n}{i} \right\rfloor\) 的值不是一直在变的,它是一段一段的,像一个个平台。当 \(i=1\) 时,值为 \(10\),当 \(i=2\) 时,值为 \(5\),当 \(i=3\) 时,值为 \(3\),当 \(i\) 在 \([4,5]\) 这个区间时,值都是 \(2\),当 \(i\) 在 \([6,10]\) 这个区间时,值都是 \(1\)。
这就是“分块”的来源。既然在一个“块”里,\(\left\lfloor \dfrac{n}{i} \right\rfloor\) 的值是相同的,何必一个一个地相加呢?可以直接用 \(块的长度 \times 块的值\) 来一次性计算整个块的总和。
现在,目标变成了:如果知道一个块的左边界 \(l\),如何快速找到这个块的右边界 \(r\)?
假设当前在块的左边界 \(l\),这个块的值是 \(k = \left\lfloor \dfrac{n}{l} \right\rfloor\)。想找最大的一个 \(r\),使得 \(\left\lfloor \dfrac{n}{r} \right\rfloor\) 的值也等于 \(k\)。
用数学语言来说,\(\left\lfloor \dfrac{n}{r} \right\rfloor = k\) 能推出 \(k \le \dfrac{n}{r}\),两边同乘以 \(r\),得到 \(k \cdot r \le n\),两边同除以 \(k\),得到 \(r \le \dfrac{n}{k}\)。
因为要找最大的整数 \(r\),所以 \(r\) 的最大值就是 \(\left\lfloor \dfrac{n}{k} \right\rfloor\)。把 \(k = \left\lfloor \dfrac{n}{l} \right\rfloor\) 代入,就得到了公式:\(r = \left\lfloor \dfrac{n}{\left\lfloor \dfrac{n}{l} \right\rfloor} \right\rfloor\)。由于编程语言中的整数除法,公式可以写成 r = n / (n / l)。
举个例子,\(n=10\):
- 第一个块:左边界 \(l=1\)。
- 块的值
k = 10 / 1 = 10 - 右边界
r = 10 / k = 10 / 10 = 1 - 这个块是 \([1,1]\)
- 块的值
- 第二个块:左边界 \(l=2\)(上一个块的 \(r+1\))。
- 块的值
k = 10 / 2 = 5 - 右边界
r = 10 / 5 = 2 - 这个块是 \([2,2]\)
- 块的值
- 第三个块:左边界 \(l=3\)。
- 块的值
k = 10 / 3 = 3 - 右边界
r = 10 / 3 = 3 - 这个块是 \([3,3]\)
- 块的值
- 第四个块:左边界 \(l=4\)。
- 块的值
k = 10 / 4 = 2 - 右边界
r = 10 / 2 = 5 - 这个块是 \([4,5]\)
- 块的值
- 第五个块:左边界 \(l=6\)。
- 块的值
k = 10 / 6 = 1 - 右边界
r = 10 / 1 = 10 - 这个块是 \([6,10]\)
- 块的值
这个算法有多快?可以证明,\(\left\lfloor \dfrac{n}{i} \right\rfloor\) 的取值最多只有 \(2 \sqrt{n}\) 种。所以最多只需要跳 \(2 \sqrt{n}\) 次。时间复杂度是 \(O(\sqrt{n})\)。
ll calc(ll n) {
ll sum = 0, l = 1;
while (l <= n) {
ll val = n / l;
ll r = n / val;
sum += (r - l + 1) * val;
l = r + 1;
}
return sum;
}
例题:P2261 [CQOI2007] 余数求和
解决问题的关键在于对取模运算 \(k \bmod i\) 进行变形。根据整数除法的定义,有:\(a \bmod b = a - b \cdot \left\lfloor \dfrac{a}{b} \right\rfloor\)。将这个关系代入原式中,得到:\(G(n,k) = \sum \limits_{i=1}^n (k - i \cdot \left\lfloor \dfrac{k}{i} \right\rfloor)\)。
可以将求和符号拆开:\(G(n,k)= \sum \limits_{i=1}^n k - \sum \limits_{i=1}^n i \cdot \left\lfloor \dfrac{k}{i} \right\rfloor\)。
第一部分 \(\sum_{i=1}^n k\) 非常简单,就是 \(n\) 个 \(k\) 相加,结果为 \(n \cdot k\)。所以原式变为:\(G(n,k) = n \cdot k - \sum \limits_{i=1}^n i \cdot \left\lfloor \dfrac{k}{i} \right\rfloor\)。
现在,问题转化为了如何快速计算 \(\sum \limits_{i=1}^n i \cdot \left\lfloor \dfrac{k}{i} \right\rfloor\),这可以用数论分块来加速计算。可以把 \(\left\lfloor \dfrac{k}{i} \right\rfloor\) 值相同的连续区间 \(i\) 作为一个“块”,把这个块进行统一计算,从而避免逐个元素计算。如果某个块的区间是 \([l,r]\),值是 \(val\),则这一块的贡献是 \(\dfrac{(l+r)(r-l+1)}{2} \cdot val\)(等差数列求和)。在分块计算时,注意边界判断。
参考代码
#include <cstdio>
using ll = long long;
ll calc(ll k, ll n) {
ll sum = 0, l = 1;
while (l <= n) {
ll val = k / l;
if (val == 0) break;
ll r = k / val;
if (r > n) r = n;
sum += (l + r) * (r - l + 1) / 2 * val;
l = r + 1;
}
return sum;
}
int main()
{
int n, k; scanf("%d%d", &n, &k);
ll ans = 1ll * n * k - calc(k, n);
printf("%lld\n", ans);
return 0;
}
习题:P3935 Calculating
解题思路
题目定义的函数 \(f(x)\) 实际上就是正整数 \(x\) 的约数个数,通常记作 \(d(x)\) 或 \(\tau(x)\)。所以,题目的核心是计算 \(\sum \limits_{i=l}^r d(i) \pmod{998244353}\)。
直接计算区间 \([l,r]\) 上的和很困难。可以利用前缀和思想,将问题转化为计算两个从 \(1\) 开始的和的差。定义一个函数 calc(n) 用于计算 \(\sum \limits_{i=1}^n d(i)\),那么原问题的答案就是 calc(r) - calc(l - 1)。
现在的问题是如何高效计算 \(\sum \limits_{i=1}^n d(i)\),直接遍历效率上是不够的,需要一个巧妙的数学变换。
考虑 \(d(i)\) 的定义:\(i\) 的约数个数。可以从另一个角度来计数:对于 \(1\) 到 \(n\) 之间的每一个整数 \(j\),它可以作为哪些数 \(i\)(其中 \(1 \le i \le n\))的约数?一个数 \(j\) 是 \(i\) 的约数,当且仅当 \(i\) 是 \(j\) 的倍数。在 \(1\) 到 \(n\) 范围内,\(j\) 的倍数有 \(j, 2j, 3j, \dots, k \cdot j\),其中 \(k \cdot j \le n\)。这样的倍数总共有 \(\left\lfloor \dfrac{n}{j} \right\rfloor\) 个。
于是就得到了 \(\sum \limits_{i=1}^n d(i) = \sum \limits_{i=1}^n \left\lfloor \dfrac{n}{i} \right\rfloor\)。于是就可以用“数论分块”进行加速计算。
参考代码
#include <cstdio>
using ll = long long;
const int MOD = 998244353;
ll calc(ll n) {
ll sum = 0, l = 1;
while (l <= n) {
ll val = n / l;
ll r = n / val;
sum += (r - l + 1) * val;
sum %= MOD;
l = r + 1;
}
return sum;
}
int main()
{
ll l, r; scanf("%lld%lld", &l, &r);
printf("%lld\n", (calc(r) + MOD - calc(l - 1)) % MOD);
return 0;
}
习题:P4863 JerryC Loves Driving
给定两个正整数 \(A,B \ (1 \le A \le B \le 2 \times 10^7)\),求出以下式子的值:\(\sum\limits_{i=A}^B \sum\limits_{j=1}^{i}\left\lfloor \frac{i}{j} \right\rfloor \times (-1)^j\)。
解题思路 $O(B)$
设 \(S(n) = \sum\limits_{i=1}^n \sum\limits_{j=1}^i \lfloor \frac{i}{j} \rfloor \times (-1)^j\),则原式的结果即为 \(S(B) - S(A-1)\)。将 \(S(n)\) 的求和顺序交换,得到 \(S(n) = \sum\limits_{j=1}^n (-1)^j \sum\limits_{i=j}^n \left\lfloor \frac{i}{j} \right\rfloor\)。
考虑内层式子 \(f(n, j) = \sum\limits_{i=j}^n \lfloor \frac{i}{j} \rfloor\),可以利用整除的性质 \(\lfloor i/j \rfloor = k \iff kj \le i \lt (k+1)j\),因此,\(\lfloor i/j \rfloor\) 的值可以看作是满足 \(kj \le i\) 的正整数 \(k\) 的个数之和。
内层求和的结果是 \(n-kj+1\),设 \(m = \lfloor n/j \rfloor\),则 \(f(n, j) = \sum\limits_{k=1}^m (n + 1 - kj) = m(n + 1) - j \times \frac{m(m + 1)}{2}\)。
有了上述公式,只需要枚举 \(j \in [1, B]\) 即可。对于每一个 \(j\),计算 \(f(B, j) - f(A-1, j)\)。注意当 \(j > A-1\) 时,\(f(A-1, j) = 0\)。总方案数为 \(\sum\limits_{j=1}^B (-1)^j \times (f(B, j) - f(A-1, j))\)。
参考代码
#include <cstdio>
using ll = long long;
int main()
{
int a, b;
scanf("%d%d", &a, &b);
ll ans = 0;
for (int i = 1; i <= b; i++) {
int m = b / i;
ll fb = 1ll * m * (b + 1) - 1ll * i * m * (m + 1) / 2;
ll fa = 0;
if (i < a) {
m = (a - 1) / i;
fa = 1ll * m * a - 1ll * i * m * (m + 1) / 2;
}
int sign = i & 1 ? -1 : 1;
ans += sign * (fb - fa);
}
printf("%lld\n", ans);
return 0;
}
解题思路 $O(\sqrt{B})$
由于 \(m = \lfloor n/j \rfloor\) 在多个连续的 \(j\) 上取值相同,可以使用数论分块,在每一个块 \([l,r]\) 内,\(m\) 是常数。只需要快速计算块内的 \(\sum (-1)^j\) 和 \(\sum j \cdot (-1)^j\):
- \(\sum\limits_{j=1}^k (-1)^j\):当 \(k\) 为偶数时为 \(0\),奇数时为 \(-1\)。
- \(\sum\limits_{j=1}^k j \cdot (-1)^j\):当 \(k\) 为偶数时为 \(k/2\),奇数时为 \(-(k+1)/2\)。
参考代码
#include <cstdio>
using ll = long long;
int p0(int k) {
return k % 2 == 0 ? 0 : -1;
}
int p1(int k) {
return k % 2 == 0 ? k / 2 : -(k + 1) / 2;
}
ll solve(int n) {
ll res = 0;
int l = 1;
while (l <= n) {
int m = n / l;
int r = n / m;
int p = p0(r) - p0(l - 1);
int q = p1(r) - p1(l - 1);
res += 1ll * m * (n + 1) * p - 1ll * m * (m + 1) / 2 * q;
l = r + 1;
}
return res;
}
int main()
{
int a, b;
scanf("%d%d", &a, &b);
printf("%lld\n", solve(b) - solve(a - 1));
return 0;
}

浙公网安备 33010602011771号