数论分块

计算这个式子的和:\(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\) 的个数之和。

\[f(n, j) = \sum\limits_{i=1}^n \sum\limits_{k=1}^{\infty} [kj \le i] = \sum\limits_{k=1}^{\lfloor n/j \rfloor} \sum\limits_{i=kj}^n 1 \]

内层求和的结果是 \(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;
}
posted @ 2025-07-15 00:23  RonChen  阅读(95)  评论(0)    收藏  举报