题目链接
题目描述
给定两个正整数 和
(
),求值在
区间内的所有整数的因数个数之和。形式化地,就是计算
的值,其中
表示正整数
的因数个数。
解题思路
本题要求计算区间 内所有数的因数个数之和。直接对区间内每个数进行因数分解并计数,效率会很低。一个常见的优化思路是利用前缀和思想,将问题转化为计算两个从
开始的区间的差:
这样,问题就变成了如何快速计算函数 。
让我们换一个角度来思考 的计算。与其枚举每个数
并计算它的因数个数
,我们可以反过来考虑每个数
(从
到
) 作为因数,对总和贡献了多少次。
一个数 是
的因数,其中
。在
到
的范围内,
的倍数一共有
个。这意味着,数字
会作为因数出现
次,每次都为因数总和贡献
。
因此,我们可以得到一个新的求和公式:
现在问题变成了如何快速计算 。直接遍历
从
到
的时间复杂度是
,对于很大的
依然会超时。
这里就可以引入整除分块(也叫数论分块)技巧了。我们观察到,在 的连续变化过程中,
的值会保持不变,形成一个个“块”。例如,当
时:
,
,
,
,
,
对于任意一个块的起始位置 ,该块内所有
对应的
的值都等于
。这个块的结束位置
是满足
的最大整数,可以被证明为
。
于是,我们可以按块进行迭代。对于每个从 到
的块,它包含的项数是
,每项的值都是
。该块对总和的贡献就是
。计算完一个块后,下一个块的起始位置就是
。
这种分块方法的迭代次数大约是 级别,所以计算
的时间复杂度可以优化到
,足以通过本题。
最终,我们分别计算 和
,相减即可得到答案。
代码
#include <iostream>
using namespace std;
// 使用整除分块计算 S(n) = sum_{i=1 to n} floor(n/i)
long long calculate(long long n) {
if (n == 0) {
return 0;
}
long long ans = 0;
for (long long l = 1, r; l <= n; l = r + 1) {
long long val = n / l;
r = n / val;
ans += (long long)(r - l + 1) * val;
}
return ans;
}
int main() {
long long l, r;
cin >> l >> r;
cout << calculate(r) - calculate(l - 1) << endl;
return 0;
}
import java.util.Scanner;
public class Main {
// 使用整除分块计算 S(n) = sum_{i=1 to n} floor(n/i)
private static long calculate(long n) {
if (n == 0) {
return 0;
}
long ans = 0;
for (long l = 1, r; l <= n; l = r + 1) {
long val = n / l;
r = n / val;
ans += (long) (r - l + 1) * val;
}
return ans;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
long l = sc.nextLong();
long r = sc.nextLong();
System.out.println(calculate(r) - calculate(l - 1));
}
}
import sys
# 使用整除分块计算 S(n) = sum_{i=1 to n} floor(n/i)
def calculate(n):
if n == 0:
return 0
ans = 0
l = 1
while l <= n:
val = n // l
r = n // val
ans += (r - l + 1) * val
l = r + 1
return ans
def solve():
l, r = map(int, input().split())
result = calculate(r) - calculate(l - 1)
print(result)
solve()
算法及复杂度
- 算法:数论、整除分块
- 时间复杂度:我们使用整除分块计算两次前缀和,单次计算的复杂度为
。因此,总时间复杂度为
。
- 空间复杂度:
,没有使用额外的与输入规模相关的存储空间。