数论分块

计算这个式子的和:\(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;
}

习题: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;
}

习题: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;
}
posted @ 2025-07-15 00:23  RonChen  阅读(43)  评论(0)    收藏  举报