跳转至

数论分块

数论分块可以快速计算一些含有除法向下取整的和式(即形如 i=1nf(i)g(ni) 的和式)。当可以在 O(1) 内计算 f(r)f(l) 或已经预处理出 f 的前缀和时,数论分块就可以在 O(n) 的时间内计算上述和式的值。

它主要利用了富比尼定理(Fubini's theorem),将 ni 相同的数打包同时计算。

富比尼定理

又称「算两次」,以意大利数学家圭多·富比尼(Guido Fubini)命名。 富比尼定理的积分形式:只要二重积分 |f(x,y)|dxdy 有界,则可以逐次计算二重积分,并且可以交换逐次积分的顺序。 积分号也是特殊的求和号,因此在一般求和中,富比尼定理往往呈现为更换计数顺序,即交换两个求和号。 组合数学中的富比尼定理表现为,用两种不同的方法计算同一个量,从而建立相等关系。

例如这里的双曲线下整点的图片:

双曲线下整点

图中共分为了 5 块,这 5 块整点的最大纵坐标都相同。如果统计整点的个数,可以从纵向计数改为横向计数,直接计算 5 个矩形即可。

引理 1

a,b,cZ,abc=abc

略证:

ab=ab+r(0r<1)abc=ab1c=1c(ab+r)=abc+rc=abc
关于证明最后的小方块

QED 是拉丁词组「Quod Erat Demonstrandum」(这就是所要证明的)的缩写,代表证明完毕。现在的 QED 符号通常是 或者 。(维基百科

引理 2

nN+,|{nddN+,dn}|2n

|V| 表示集合 V 的元素个数

略证:

对于 dnndn 种取值

对于 d>n,有 ndn,也只有 n 种取值

综上,得证

数论分块结论

对于常数 n,使得式子

ni=nj

成立且满足 ijnj 值最大为 nni,即值 ni 所在块的右端点为 nni

证明

k=ni,可以知道 kni

nknni=i=ij=max满足条件的所有 i=imax=nk=nni

过程

数论分块的过程大概如下:考虑和式

i=1nf(i)ni

那么由于我们可以知道 ni 的值成一个块状分布(就是同样的值都聚集在连续的块中),那么就可以用数论分块加速计算,降低时间复杂度。

利用上述结论,我们先求出 f(i)前缀和(记作 s(i)=j=1if(j)),然后每次以 [l,r]=[l,nni] 为一块,分块求出贡献累加到结果中即可。

伪代码如下:

1Calculate s(i), the prefix sum of f(i).2l13r04result05while ln do:6rnn/l7resultresult+[s(r)s(l1)]×nl8lr+19end while 

最终得到的 result 即为所求的和式。

例题:UVa11526 H(n)

题意:T 组数据,每组一个整数 n。对于每组数据,输出 i=1nni

思路

如上推导,对于每一块相同的 ni 一起计算。时间复杂度为 O(Tn)

实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

long long H(int n) {
  long long res = 0;  // 储存结果
  int l = 1, r;       // 块左端点与右端点
  while (l <= n) {
    r = n / (n / l);  // 计算当前块的右端点
    // 累加这一块的贡献到结果中。乘上 1LL 防止溢出
    res += 1LL * (r - l + 1) * (n / l);
    l = r + 1;  // 左端点移到下一块
  }
  return res;
}

int main() {
  std::ios::sync_with_stdio(false);
  std::cin.tie(nullptr);
  int t, n;
  std::cin >> t;
  while (t--) {
    std::cin >> n;
    std::cout << H(n) << '\n';
  }
  return 0;
}

向上取整的数论分块

向上取整与前文所述的向下取整十分类似,但略有区别:

对于常数 n,使得式子

ni=nj

成立且满足 ijnj 值最大为 n1n1i,即值 ni 所在块的右端点为 n1n1i

注意

i=n 时,上式会出现分母为 0 的错误,需要特殊处理。

证明

ni=n1i+1,可以发现 n 的上取整分块与 n1 的下取整分块是一样的。

例题:CF1954E Chain Reaction

题意:有一排 n 个怪兽,每个怪兽初始血量为 ai,一次攻击会使一段连续的存活的怪兽血量减 k,血量不大于 0 视作死亡,对于所有 k 求出击杀所有怪兽所需攻击次数,n,ai105

思路

下面是一种使用二维数论分块的解法:

使用 积木大赛 的技巧,令 a0=0,对于某个 k,答案就是 i=1nmax(0,aikai1k)

对于相邻的两个怪兽,使用二维数论分块,分段求出它们对一段 k 的答案的贡献,然后差分累加即可。

复杂度 O(ai)。也存在其他解法。

实现
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <algorithm>
#include <iostream>

constexpr int N = 100009;
int n, a[N], maxn;
long long ans[N];

int main() {
  std::ios::sync_with_stdio(false);
  std::cin.tie(nullptr);
  std::cin >> n;
  for (int i = 1; i <= n; ++i) {
    std::cin >> a[i];
    maxn = std::max(maxn, a[i]);
  }
  for (int i = 0; i < n; ++i)
    for (int l = 1, r;; l = r + 1) {
      r = std::min(l < a[i] ? (a[i] - 1) / ((a[i] - 1) / l) : N,
                   l < a[i + 1] ? (a[i + 1] - 1) / ((a[i + 1] - 1) / l)
                                : N);  // 二维数论分块
      if (r == N) break;
      int x = (a[i + 1] - 1) / l - std::max(a[i] - 1, 0) / l;
      if (x > 0) ans[l] += x, ans[r + 1] -= x;  // 累加贡献
    }
  ++ans[0];  // ⌈a/l⌉=(a-1)/l+1的式子当a=0时不成立,需要修正
  for (int i = 1; i <= maxn; ++i)
    std::cout << (ans[i] += ans[i - 1]) << " \n"[i == maxn];
  return 0;
}

N 维数论分块

求含有 a1ia2iani 的和式时,数论分块右端点的表达式从一维的 ni 变为 minj=1n{aji},即对于每一个块的右端点取最小(最接近左端点)的那个作为整体的右端点。可以借助下图理解:

多维数论分块图解

一般我们用的较多的是二维形式,此时可将代码中 r = n / (n / i) 替换成 r = min(n / (n / i), m / (m / i))

数论分块扩展

以计算含有 nd 的和式为例。考虑对于一个正整数 n,如何求出集合

S={nddN+,dn}

的所有值,以及对每一种值求出哪些 d 会使其取到这个值。可以发现:

  1. 因为 nd 是单调不增的,所以对于所有 vS,使得 nd=vd 必然是一段区间。
  2. 对于任意正整数 tn,我们对 t>tvS 分别分析,可以发现 t+n/t2|S|,取 t=n3 得到 |S| 的一个上界为 O(n3)

这些结论与数论分块所需的引理相似,因此猜测可以写为数论分块形式。

结论是:使得式子

np=nq

成立的最大的 q 满足 pqn

nn/p2
证明

v=np=nq,那么

vnqv2nqqnv2qnv2

同理 pn/v2。同时

nn/v2nn/v2=v=v

又由 pn/v2 以及单调性可推出

v=npnn/v2

所以

nn/v2=v

进而 q=n/v2 是最大的使得 n/p=n/q 成立的 q

故原问题可以写为数论分块形式,代码与数论分块形式并无二异。

两个更加通用的结论

对于正整数 n 和正实数 α,β,我们有

  1. 对于某个不超过 nα/β 的正整数 i,使式子 nαiβ=nαjβ 成立的最大的 jnα/βnα/iβ1/β
  2. 集合 {nαdβ:d=1,2,,n} 的大小不超过 min{n,2nα/(1+β)}

习题

  1. CQOI2007 余数求和(需要一点转化和特判)

  2. UVa11526 H(n)(几乎可以当做模板题)

  3. POI2007 ZAP-Queries(数论分块一般配合 莫比乌斯反演 用以进一步降低复杂度;本题需要用到 [n=1]=d|nμ(d) 这一条莫反结论)