【学习笔记】数论分块(整除分块)
先看一个例子:
给出正整数 \(n(n \leq 10^{12})\),计算:
如果直接暴力,复杂度为 \(O(n)\),无法在 1s 内通过,但使用数论分块(整除分块)可以将复杂度降至 \(O(\sqrt{n})\)。
先看个例子,当 \(n = 100\) 时,\(i\) 和 \(\lfloor \frac{n}{i} \rfloor\) 的情况如下:
$i \in $ | $\lfloor \frac{n}{i} \rfloor = $ |
---|---|
\([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\) |
我们发现,\(\lfloor \frac{n}{i} \rfloor\) 的取值很少,在本例 \(n = 100\) 中只有 \(19\) 种取值。事实上,对于任意 \(n\),\(\lfloor \frac{n}{i} \rfloor\) 的取值不会超过 \(2 \sqrt{n}\) 种。
有一个显然的结论,所有相等的 \(\lfloor \frac{n}{i} \rfloor\) 对应的 \(i\) 是连续的。因此,我们把这段连续的 \(i\) 作为一个“块”来处理(例如 \([26,33]\) 就是一个块)。
由上表格,\(n = 100\) 时总共有 \(19\) 个块。已经证明,块的数量不超过 \(2 \sqrt{n}\),即 \(O(\sqrt{n})\) 级别。
现在的问题在于,在确定了一个块的左端点 \(L\) 之后,如何确定块的右端点 \(R\)?
可以证明:
所以,我们初始设 \(L = 1\),每次将 \(ans\) 加 \(\lfloor \frac{n}{L} \rfloor \times (R - L + 1)\),最后将 \(L\) 设为 \(R + 1\) 再进入下一轮循环。
例题1:余数求和
本题需要用到取模的性质 \(k \bmod i = k - i \times \lfloor \frac{k}{i} \rfloor\),这样原题就转化为求
考虑用数论分块求后一项。其余是一样的套路,只需要每次将 \(ans\) 减
即
另外,因为 \(n,k\) 的大小关系不确定,所以当 \(L > k\) 时 \(\lfloor \frac{k}{L} \rfloor = 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;
}