【学习笔记】数论分块(整除分块)

先看一个例子:

给出正整数 n(n1012),计算:

i=1nni

如果直接暴力,复杂度为 O(n),无法在 1s 内通过,但使用数论分块(整除分块)可以将复杂度降至 O(n)

先看个例子,当 n=100 时,ini 的情况如下:

i ni=
[1,1] 100
[2,2] 50
[3,3] 33
[4,4] 25
[5,5] 20
[6,6] 16
[7,7] 14
[8,8] 12
[9,9] 11
[10,10] 10
[11,11] 9
[12,12] 8
[13,14] 7
[15,16] 6
[17,20] 5
[21,25] 4
[26,33] 3
[34,50] 2
[51,100] 1

我们发现,ni 的取值很少,在本例 n=100 中只有 19 种取值。事实上,对于任意 nni 的取值不会超过 2n 种。

证明如下

有一个显然的结论,所有相等的 ni 对应的 i 是连续的。因此,我们把这段连续的 i 作为一个“块”来处理(例如 [26,33] 就是一个块)。

由上表格,n=100 时总共有 19 个块。已经证明,块的数量不超过 2n,即 O(n) 级别。

现在的问题在于,在确定了一个块的左端点 L 之后,如何确定块的右端点 R

可以证明:

R=nnL

证明如下

所以,我们初始设 L=1,每次将 ansnL×(RL+1),最后将 L 设为 R+1 再进入下一轮循环。

例题1:余数求和

本题需要用到取模的性质 kmodi=ki×ki,这样原题就转化为求

n×ki=1ni×ki

考虑用数论分块求后一项。其余是一样的套路,只需要每次将 ans

kL×i=Lri

kL×(L+R)×(RL+1)2

另外,因为 n,k 的大小关系不确定,所以当 L>kkL=0,可以直接退出循环。

#include<bits/stdc++.h>
#define int unsigned long long
using namespace std;
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	int n,k,l,r;
	cin >> n >> k;
	int ans = n * k;
	for(l = 1;l <= n;l = r + 1){
		if(k >= l)
			r = min(k / (k / l),n);
		else
			break;
		ans -= ((r - l + 1) * (int)floor(k / l) * (l + r)) >> 1;
	}
	cout << ans;
	return 0;
}
posted @   5t0_0r2  阅读(16)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示