数论分块
介绍
- 数论分块可以快速计算一些含有除法向下取整的和式(即形如 ∑ i = 1 n f ( i ) g ( ⌊ n i ⌋ ) \sum_{i=1}^nf(i)g(\left\lfloor\dfrac n i\right\rfloor) ∑i=1nf(i)g(⌊in⌋) 的和式)。当可以在 O ( 1 ) O(1) O(1) 内计算 f ( r ) − f ( l ) f(r)-f(l) f(r)−f(l) 或已经预处理出 f f f 的前缀和时,数论分块就可以在 O ( n ) O(\sqrt n) O(n) 的时间内计算上述和式的值。
引入
- 思考如何求解 ∑ i = 1 n ⌊ n i ⌋ \sum_{i=1}^n\left\lfloor\dfrac ni\right\rfloor ∑i=1n⌊in⌋, 1 ≤ n ≤ 1 0 12 1\leq n \leq 10^{12} 1≤n≤1012
- 暴力 O ( n ) O(n) O(n) 枚举显然是不行的,但如果一时没有思路,不妨先尝试通过暴力发现一些什么:
- 对于 n = 11 n=11 n=11, ⌊ n i ⌋ = { 11 , 5 , 3 , 2 , 2 , 1 , 1 , 1 , 1 , 1 , 1 } \left\lfloor\dfrac ni\right\rfloor = \{ 11,5,3,2,2,1,1,1,1,1,1 \} ⌊in⌋={11,5,3,2,2,1,1,1,1,1,1}
- 对于 n = 30 n=30 n=30, ⌊ n i ⌋ = { 30 , 15 , 10 , 7 , 6 , 5 , 4 , 3 , 3 , 3 , 2 , 2 , 2 , 2 , 2 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 } \left\lfloor\dfrac ni\right\rfloor = \{30,15,10,7,6,5,4,3,3,3,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 \} ⌊in⌋={30,15,10,7,6,5,4,3,3,3,2,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1}
- 显而易见,数列中有几段数值相同的连续部分,即对于同一个 n n n,有不少 i i i 使得 ⌊ n i ⌋ \left\lfloor\dfrac n i\right\rfloor ⌊in⌋ 的值相同,没有必要多次计算,于是整除分块便出现了。如何快速计算每一个数值对应的区间呢?
- 对于枚举的每个区间的左端点 l l l,首先易得该区间的数值均为 ⌊ n l ⌋ \left\lfloor\dfrac n l\right\rfloor ⌊ln⌋,由结论可知区间右端点 r = ⌊ n ⌊ n l ⌋ ⌋ r=\left\lfloor\dfrac n{\lfloor\frac n l\rfloor}\right\rfloor r=⌊⌊ln⌋n⌋
时间复杂度
- O ( n ) O(\sqrt n) O(n). 可以证明块数不大于 2 n 2\sqrt n 2n。简单来说可以分类讨论一下,若 i ≤ n i \leq \sqrt n i≤n, i i i 最多可以有 n \sqrt n n 种取值,因此 ⌊ n i ⌋ \left\lfloor\dfrac n i\right\rfloor ⌊in⌋ 最多有 n \sqrt n n 种取值;若 i > n i > \sqrt n i>n,显然 ⌊ n i ⌋ \left\lfloor\dfrac n i\right\rfloor ⌊in⌋ 最多有 n \sqrt n n 种取值。综上, ⌊ n i ⌋ \left\lfloor\dfrac n i\right\rfloor ⌊in⌋ 最多有 2 n 2\sqrt n 2n 种取值。整个算法时间复杂度为 O ( n ) O(\sqrt n) O(n)。
例题1:约数个数
题目描述:
设 σ ( x ) σ(x) σ(x) 表示 x x x 的约数个数,给定 n n n ( 1 ≤ n < 2 32 ) (1\leq n < 2^{32}) (1≤n<232),求 ∑ i = 1 n σ ( i ) \sum_{i=1}^nσ(i) ∑i=1nσ(i)
分析
- 数学语言: ∑ i = 1 n σ ( i ) = ∑ i = 1 n s u m ( d ∣ i ) = ∑ d = 1 n ⌊ n d ⌋ \sum_{i=1}^n σ(i)=\sum_{i=1}^n sum(d|i)=\sum_{d=1}^n\left\lfloor\dfrac n d\right\rfloor ∑i=1nσ(i)=∑i=1nsum(d∣i)=∑d=1n⌊dn⌋
- 也就是说,相较于朴素思想即统计 [ 1 , n ] [1,n] [1,n] 每个数的约数个数,我们转化为考虑每一个约数 d ∈ [ 1 , n ] d \in [1,n] d∈[1,n] 在区间中的总贡献(某个约数在所有数的约数集合中出现的总次数),对于总贡献相同的区间(某些约数的出现次数相同),答案累加 区间长度乘上出现次数 即可。
代码
void solve() {
ll n; cin >> n;
ll ans = 0;
for(ll l=1,r;l<=n;l=r+1) { // 枚举区间左端点
r = n / (n / l); // 计算区间右端点
ll h = n / l; // 该区间约数的相同出现次数
ans += h*(r-l+1);
}
cout << ans;
}
例题2:约数和
题目描述:
设 σ ( x ) σ(x) σ(x) 表示 x x x 所有约数的和,给定 x , y x,y x,y ( 1 ≤ x < y ≤ 2 × 1 0 9 ) (1\leq x < y \leq 2\times 10^9) (1≤x<y≤2×109),求 ∑ i = x y σ ( i ) \sum_{i=x}^yσ(i) ∑i=xyσ(i)
分析
- 数学语言: ∑ i = 1 n σ ( i ) = ∑ i = 1 n ∑ d ∣ x d = ∑ d = 1 n d × ⌊ n d ⌋ \sum_{i=1}^n σ(i)=\sum_{i=1}^n \sum_{d|x}d=\sum_{d=1}^n d\times \left\lfloor\dfrac n d\right\rfloor ∑i=1nσ(i)=∑i=1n∑d∣xd=∑d=1nd×⌊dn⌋
- 基本思想与例题1大同小异,注意记录的是约数的和,考虑 [ l , r ] [l,r] [l,r] 约数区间和。
- 另外本题求解的是 [ x , y ] [x,y] [x,y] 中数的约数和,利用前缀和的思想分别求解 f y f_y fy 和 f x − 1 f_{x-1} fx−1,作差即为答案。
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll solve(int n) {
ll ans = 0;
for(ll l=1,r;l<=n;l=r+1) {
r = n / (n / l);
ll h = n / l;
ans += h*(l+r)*(r-l+1)/2; // 等差数列求和
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int x,y; cin >> x >> y;
cout << solve(y)-solve(x-1) << "\n";
return 0;
}
例题3:余数求和
题目描述:
给定正整数 n , k n,k n,k,求 G ( n , k ) = ∑ i = 1 n k m o d i G(n,k) = \sum_{i=1}^n k \mod i G(n,k)=∑i=1nkmodi
分析
- 对于取模运算,可以转化: ∑ i = 1 n k m o d i = ∑ i = 1 n k − i × ⌊ k i ⌋ = n × k − ∑ i = 1 n i × ⌊ k i ⌋ \sum_{i=1}^n k \mod i=\sum_{i=1}^n k-i\times \left\lfloor\dfrac k i\right\rfloor=n\times k-\sum_{i=1}^n i\times \left\lfloor\dfrac k i\right\rfloor ∑i=1nkmodi=∑i=1nk−i×⌊ik⌋=n×k−∑i=1ni×⌊ik⌋
- 对于后面一项,我们发现它与 ∑ i = 1 n f ( i ) g ( ⌊ n i ⌋ ) \sum_{i=1}^nf(i)g(\left\lfloor\dfrac n i\right\rfloor) ∑i=1nf(i)g(⌊in⌋) 形式上十分接近,可以考虑整除分块,不同的是分子部分为 k k k 而不是 n n n。
- 考虑 n > k n>k n>k,对于 ∀ i > k \forall i>k ∀i>k,有 ⌊ k i ⌋ = 0 \left\lfloor\dfrac k i\right\rfloor=0 ⌊ik⌋=0,故 ∑ i = 1 n i × ⌊ k i ⌋ = ∑ i = 1 k i × ⌊ k i ⌋ + ∑ i = k + 1 n i × ⌊ k i ⌋ = ∑ i = 1 k i × ⌊ k i ⌋ + 0 = ∑ i = 1 k i × ⌊ k i ⌋ \sum_{i=1}^n i\times \left\lfloor\dfrac k i\right\rfloor=\sum_{i=1}^k i\times \left\lfloor\dfrac k i\right\rfloor+\sum_{i=k+1}^n i\times \left\lfloor\dfrac k i\right\rfloor=\sum_{i=1}^k i\times \left\lfloor\dfrac k i\right\rfloor+0=\sum_{i=1}^k i\times \left\lfloor\dfrac k i\right\rfloor ∑i=1ni×⌊ik⌋=∑i=1ki×⌊ik⌋+∑i=k+1ni×⌊ik⌋=∑i=1ki×⌊ik⌋+0=∑i=1ki×⌊ik⌋,所以左边界枚举到 n , k n,k n,k 的较小值即可。同时,右边界不能超过 n n n。
代码
void solve() {
ll n, k; cin >> n >> k;
ll res = 0;
for(ll l=1,r;l<=min(n,k);l=r+1) {
r = min(n,k/(k/l));
ll h = k / l;
res += h * (l+r)*(r-l+1)/2;
}
cout << n*k-res << "\n";
}