题目链接
题目描述
对于给定的正整数 和
(
),求表达式
的值。
解题思路
本题要求计算 。
首先,一个直接的想法是循环 从
到
,累加每一个
的值。但由于
的范围可以达到
,这种
的朴素算法显然会超时。我们需要寻找更高效的方法。
我们可以利用取模运算和整除运算之间的关系来进行数学变换。我们知道,对于任意正整数 和
,有:
将这个关系代入原求和式中,得到:
我们可以将这个和式拆分为两部分:
现在,问题转化为了如何快速计算 。
这个和式的关键部分是 。我们在此前的题目中已经知道,
的值在
连续变化时,会在很多区间内保持不变。这提示我们可以使用整除分块技巧。
我们按 的值进行分块。对于一个块,其起始位置为
,块内所有的
都有相同的
值,我们记为
。这个块的结束位置
是满足
的最大整数,可以计算得出
。同时,由于我们的求和上限是
,所以块的实际结束位置应该是
。
对于从 到
的这一个块,它对
的贡献是:
其中 是一个等差数列求和,其值为
。
因此,我们可以通过迭代这些块来计算 。从
开始,计算出当前块的
和
,累加该块的贡献,然后将下一个块的起始位置更新为
,直到
。
当 时,
,这些项对
的贡献为零。所以我们的分块循环实际上最多只会进行到
。整个算法的时间复杂度大约是
,对于
的数据规模是完全可以接受的。
代码
#include <iostream>
#include <algorithm>
using namespace std;
int main() {
long long n, k;
cin >> n >> k;
long long sum_f = 0;
for (long long l = 1, r; l <= n; l = r + 1) {
long long v = k / l;
if (v == 0) {
// 当 k/l 等于0时,后续所有项的 i * floor(k/i) 也都为0,可以提前结束
break;
}
r = min(n, k / v);
// 等差数列求和: (首项 + 末项) * 项数 / 2
long long sum_i = (l + r) * (r - l + 1) / 2;
sum_f += v * sum_i;
}
long long ans = n * k - sum_f;
cout << ans << endl;
return 0;
}
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
long n = sc.nextLong();
long k = sc.nextLong();
long sumF = 0;
for (long l = 1, r = 0; l <= n; l = r + 1) {
long v = k / l;
if (v == 0) {
// 当 k/l 等于0时,后续所有项的 i * floor(k/i) 也都为0,可以提前结束
break;
}
r = Math.min(n, k / v);
// 等差数列求和: (首项 + 末项) * 项数 / 2
long sumI = (l + r) * (r - l + 1) / 2;
sumF += v * sumI;
}
long ans = n * k - sumF;
System.out.println(ans);
}
}
import sys
def solve():
n, k = map(int, input().split())
sum_f = 0
l = 1
while l <= n:
v = k // l
if v == 0:
# 当 k/l 等于0时,后续所有项的 i * floor(k/i) 也都为0,可以提前结束
break
r = min(n, k // v)
# 等差数列求和: (首项 + 末项) * 项数 / 2
sum_i = (l + r) * (r - l + 1) // 2
sum_f += v * sum_i
l = r + 1
ans = n * k - sum_f
print(ans)
solve()
算法及复杂度
- 算法:数论、整除分块
- 时间复杂度:核心计算是整除分块,其迭代次数取决于
的不同取值个数,大约为
。同时循环不超过
。因此总时间复杂度为
。
- 空间复杂度:
,没有使用额外的与输入规模相关的存储空间。