[CQOI2007] 余数求和

OJ: luogu

题目 ID: P2261

标签: 数论整除分块

日期: 2025-12-17 15:37

1 数学推导:将模运算转化为除法

题目要求计算:

Ans=i=1n(kmodi)\text{Ans} = \sum_{i=1}^n (k \bmod i)

直接计算模运算很难进行优化。我们需要利用模运算的定义公式:

amodb=ab×aba \bmod b = a - b \times \lfloor \frac{a}{b} \rfloor

核心: 这个公式的含义是: ab的余数,就是a里面去除整块的b,成功的把余数问题转成整除问题

将这个公式代入原式:

Ans=i=1n(ki×ki)\text{Ans} = \sum_{i=1}^n (k - i \times \lfloor \frac{k}{i} \rfloor)

利用求和符号的线性性质,我们可以把式子拆成两部分:

Ans=i=1nki=1n(i×ki)\text{Ans} = \sum_{i=1}^n k - \sum_{i=1}^n (i \times \lfloor \frac{k}{i} \rfloor)

第一部分 i=1nk\sum_{i=1}^n k 很简单,就是 nnkk 相加:

Ans=n×ki=1n(i×ki)\text{Ans} = n \times k - \sum_{i=1}^n (i \times \lfloor \frac{k}{i} \rfloor)

现在的核心任务变成了快速计算减号后面的部分:

Sum=i=1n(i×ki)\text{Sum} = \sum_{i=1}^n (i \times \lfloor \frac{k}{i} \rfloor)


2 整除分块的应用

观察式子 (i×ki)\sum (i \times \lfloor \frac{k}{i} \rfloor),我们发现 ki\lfloor \frac{k}{i} \rfloor 的值呈块状分布。 对于同一个块区间 [l,r][l, r]ki\lfloor \frac{k}{i} \rfloor 的值是一个常数,设为 val

那么在区间 [l,r][l, r] 内,这一段的贡献是:

j=lr(j×val)=val×j=lrj\sum_{j=l}^r (j \times \text{val}) = \text{val} \times \sum_{j=l}^r j

这就变成了:(当前块的常数值) ×\times (当前块下标 ii 的等差数列和)

等差数列求和公式大家都很熟悉:

j=lrj=(l+r)×(rl+1)2\sum_{j=l}^r j = \frac{(l+r) \times (r-l+1)}{2}


3 代码深度解析

现在我们对照你的代码来看每一行在做什么。

变量定义与初始化

cpp
// ans 用来存储后面那一坨 ∑(i * floor(k/i)) 的值
// 注意最后要用 n*k 减去它
ll ans = 0; 

循环结构 (整除分块核心)

cpp
// 循环范围:只枚举到 min(n, k)
// 为什么?
// 1. 如果 i > n,题目不要求计算。
// 2. 如果 i > k,那么 floor(k/i) = 0,对减数部分没有贡献,不需要算。
for(int l=1,r; l <= min(k,n); l = r+1){
    
    // 1. 确定当前块的右端点 r
    // 公式 r = k / (k / l) 是整除分块的标准公式
    // 如果算出来的 r 超过了 n,我们要把它截断在 n,因为题目只要算到 n
    // 但是因为循环条件里已经写了 min(k,n),这里的 r = min(r, n) 
    // 主要是为了防止 r 跳出 n 的边界(当 n < k 时)
    r = k/(k/l); 
    if (r > n) r = n; // 你的代码写的是 r = min(r, n),效果一样
    
    // 2. 计算当前块的贡献
    // 贡献 = 值 * 区间下标和
    // 值 = (k/l)
    // 区间下标和 = (l+r)*(r-l+1)/2  <-- 等差数列求和
    ans += (ll)(k/l)*(l+r)*(r-l+1)/2;
}

最终计算

cpp
// 套用最开始推导的公式: Ans = n*k - Sum
ans = (ll)n*k - ans;
printf("%lld\n",ans);

4 几个关键细节 QA

Q1: 为什么要用 min(n, k) 作为循环上限?

  • nkn \le k:我们只需要算到 nn,所以循环到 nn 结束。
  • n>kn > k:对于 i>ki > k 的部分,ki=0\lfloor \frac{k}{i} \rfloor = 0。也就是说,在减号后面的式子里,这部分全是 00,不需要减。所以我们只需要算出 11kk 的减数和即可。

Q2: 为什么你的代码里有 r = min(r, n) 这种逻辑? 虽然循环条件限制了 l <= min(k,n),但是计算出来的 r (即 k/(k/l)) 可能会直接跳过 nn。 比如 n=5,k=10n=5, k=10。 当 l=1l=1 时,10/1=10\lfloor 10/1 \rfloor = 10,计算出的右端点 r=10/(10/1)=10r = 10/(10/1) = 10。 但是题目只让你算到 n=5n=5。如果不取 min,你就会把 6,7,8,9,106,7,8,9,10 的贡献也算进去了,导致错误。所以必须强行把右端点限制在 nn 以内。 (注:你的代码里 r = min(r,n) 写得很对,但由于你循环条件里写了 min(k,n),其实如果 n>kn > kr 不会超过 nn;主要是防 n<kn < k 的情况)

Q3: 数据范围与类型 题目中 n,k109n, k \le 10^9

  • 计算 n * k 会达到 101810^{18} 级别,必须用 long long
  • 等差数列求和 (l+r)*(r-l+1)/2 也会很大,必须强制转换 (ll) 计算。

5 复杂度分析

  • 时间复杂度:我们知道整除分块的块数是 O(k)O(\sqrt{k}) 级别的。所以整个算法的时间复杂度是 O(k)O(\sqrt{k})
    • k=109k=10^9 时,k31622\sqrt{k} \approx 31622,这在 1秒 的时限内绰绰有余。
  • 空间复杂度:O(1)O(1)

总结

你的代码逻辑非常清晰,是一个标准的整除分块模板。

  1. 转化: 将模运算转化为除法减法。
  2. 分块: 利用 ki\lfloor \frac{k}{i} \rfloor 的性质加速计算。
  3. 求和: 结合等差数列公式计算区间贡献。

这道题是后续学习 莫比乌斯反演杜教筛 的基础,务必熟练掌握 l = r + 1r = k / (k / l) 这一套写法。

代码

cpp
/* author: Rainboy  email: rainboylvx@qq.com  time: 2020年 06月 30日 星期二 16:17:16 CST */
#include <bits/stdc++.h>
using namespace std;

const int maxn = 1e5+5;
typedef long long ll;

int n,m;
int k,c;

void init(){
    scanf("%d%d",&n,&k);
    ll ans = 0;
    for(int l=1,r;l<=min(k,n);l = r+1){
        r = k/(k/l);
        r = min(r,n);
        ans += (ll)(k/l)*(l+r)*(r-l+1)/2;
    }
    ans = (ll)n*k - ans;
    printf("%lld\n",ans);
}

int main(){
    init();
    return 0;
}