整除分块

整除分块

为什么放在 \(9.\) 这个板块捏?

感觉前面很多地方都会 浅浅涉及

建议阅读 数论函数基础 章节,了解基本概念与先要知识


又称 数论分块,因其解决的问题 与整除密切相关 而得名,常用于求解形如

\[\begin {aligned} \sum _ {i = 1} ^ n { f (i) g \left ( \left \lfloor \dfrac n i \right \rfloor \right ) } \end {aligned} \]

这样的 和式,必要前提是 \(f\)前缀和 可以快速计算,或 已经得出


此处获取本节调试数据 / 代码包

全文 绝大多数 内容是对 [0] 中讲述的 粗略抄写胡乱加工


1. 原理

容易证明,不同的 \(\left \lfloor \dfrac n i \right \rfloor\) 的取值只有至多 \(2 \sqrt n\)

\(i \le \sqrt n\) 时,至多 \(\sqrt n\) 种 不同取值

\(i > \sqrt n\) 时,\(\dfrac n i < \sqrt n\),同样至多 \(\sqrt n\) 种 不同取值

故枚举不同取值,乘上 \(f\) 中对应的 一段 就可以得到答案,通常复杂度 \(O (\sqrt n)\)

这样变换后的和式可以表示成 下面的形式

\[\begin {aligned} \sum _ d g (d) \sum _ {i = l} ^ r f (i) \end {aligned} \]

现在 问题的关键 来到了如何 迅速求得 \(l, r\) 的值,即使得 \(\left \lfloor \dfrac n i \right \rfloor = d\)\(i\) 上下界

转化一下,容易发现 \(\left \lfloor \dfrac n i \right \rfloor = d\) 对于 \(i\)\(i (d + 1) > n\)\(id \le n\) 两条限制

于是除过去就变成 \(\left \lfloor \dfrac n {d + 1} \right \rfloor < i \le \left \lfloor \dfrac n d \right \rfloor\),这时候我们设当前枚举到一个 \(l\)

容易计算出其对应的 \(d = \left \lfloor \dfrac n i \right \rfloor\),又根据上文这样的 \(i_{\max} = \left \lfloor \dfrac n d \right \rfloor\),故 \(r = \left \lfloor \dfrac n {\left \lfloor \frac n i \right \rfloor} \right \rfloor\)

\(i\)上界不为 \(n\),需要 特殊处理

  • 若上界小于 \(n\) 时,每次 \(r\) 对上界取 \(\min\)
  • 若上界大于 \(n\) 时,\(l\) 大于 \(n\) 之后 \(\left \lfloor \dfrac n i \right \rfloor = 0\)

注意枚举到 \(l > n\) 时退出,不要用 \(r \le n\)


2. 拓展

上取整和式

考虑如果后面的 \(g\) 函数不是 向下取整\(g \left (\left \lfloor \dfrac n i \right \rfloor \right )\) 形式,而是 向上取整\(g \left (\left \lceil \dfrac n i \right \rceil \right )\) 形式呢

我们同样考虑求得 \(i\) 的上下界,\(\left \lceil \dfrac n i \right \rceil = d\) 对于 \(i\)\(id \ge n\)\(i (d - 1) < n\) 两条限制

于是有 \(\left \lceil \dfrac n d \right \rceil \le i < \left \lceil \dfrac n {d - 1} \right \rceil\),同样我们设当前枚举到一个 \(l\)

容易计算出其对应的 \(d = \left \lceil \dfrac n i \right \rceil\),有 \(r < \left \lceil \dfrac n {\left \lceil \frac n i \right \rceil - 1} \right \rceil\),但这还不太行,我们需要 等式形式

稍微变形一下,得到 \(r = \left \lfloor \dfrac {n - 1} {\left \lceil \frac n i \right \rceil - 1} \right \rfloor\) 就行了,注意 \(l= n\)分母为零,需要特判


高维数论分块

当 和式中出现多个上界,形如

\[\begin {aligned} \sum _ {i = 1} ^ n f (i) \sum _ {j = 1} ^ m g \left ( \left \lfloor \dfrac {n_j} i \right \rfloor \right ) \end {aligned} \]

我们每次令

\[\begin {aligned} r = \min _ {j = 1} ^ m \left ( \left \lfloor \dfrac {n_j} { \left \lfloor \frac {n_j} i \right \rfloor } \right \rfloor \right ) \end {aligned} \]

就行了,相当于把 每个 \(n_j\) 的所有断点均取出,故时间复杂度 \(O (\sum \sqrt {n_j})\)


3. 例题

CF1603C Extreme Extension

脑抽抽了,想了好一会儿,看了题解才懂,写的可能很抽象

容易想到 贪心策略,当限制好 \(a_i\) 的大小为 \(v\) 时,那么 \(a_i\) 应当 被分的尽量均匀

形式化的说,此时 \(a_i\) 应当被分为若干个 \(\left \lfloor \dfrac {a_i} {\left \lfloor \frac {a_i} {v} \right \rfloor} \right \rfloor\),余下的数 摊到靠后的几个上

\(\left \lfloor \dfrac {a_i} {\left \lfloor \frac {a_i} {v} \right \rfloor} \right \rfloor\) 就是其 最优情况下分裂出的数的最小值,显然,分裂次数为 \(\left \lceil \dfrac {a_i} v \right \rceil -1\)

显然,一个子段末尾 的值最优的情况是 不被分开

于是根据上述策略,我们可以得到一个 简单的 \(O (n ^ 2)\) 算法,即枚举右端点,向左倒推

显然不够优,仔细思考我们可以发现,对于一个 \(a_i\)\(\left \lfloor \dfrac {a_i} {\left \lfloor \frac {a_i} {v} \right \rfloor} \right \rfloor\) 的取值仅有 \(O (\sqrt n)\)

容易证明,对于一个 确定的取值,其 分裂次数是固定的

我们尝试将这些 相同取值的段合并到一起计算贡献

考虑设 \(f _ {i, x}\)\(a_i\) 开头,满足 \(\left \lfloor \dfrac {a_i} {\left \lfloor \frac {a_i} {v} \right \rfloor} \right \rfloor = x\)子串个数(右端点个数),考虑转移

显然,\(a _ {i + 1}\) 的分裂最小取值会 直接决定 转移到的 \(x\),但直接取 分裂最小取值相等 的段 比较困难

\(\left \lfloor \dfrac {a_i} {\left \lfloor \frac {a_i} {v} \right \rfloor} \right \rfloor\) 这个式子的相等段比较难找

故考虑找 分裂出的数个数 \(\left \lceil \dfrac {a_{i}} v \right \rceil\) 相同的段,可以用 向上取整的数论分块 实现

\(c = \left \lceil \dfrac {a_i} l \right \rceil\) 表示 分裂出数的个数\(l\) 即该区间 左端点),那么 \(a_i\) 的分裂最小取值即为 \(\left \lfloor \dfrac {a_i} {c} \right \rfloor\)

于是有 \(f _ {i, v} = \sum _ {j = l} ^ {r} f _ {i + 1, j}\)

注意到 \(f_i, f_{i + 1}\) 中其实均只有 \(O (\sqrt n)\) 个位置有取值,故复杂度可以保证

考虑 \(f_{i, v}\) 的意义,即使得 \(a_i\) 分裂最小取值为 \(v\) 这种情况的 右端点情况数

考虑贡献,显然若 子段左端点 小于等于 \(i\),则 \(f _ {i, v}\)每一种情况 都会被算进去

向左延伸不影响 \(a_i\) 的分裂情况

然后 \(f_{i, v}\) 中每一种情况,都会使得 \(a_i\)\(c - 1\) 次分裂,故其对答案的贡献为 \(i(c - 1) f _ {i, v}\)

于是我们 从右向左转移 即可,每次至多转移 \(O (\sqrt n)\) 个有值点,时间复杂度 \(O (n \sqrt n)\)

注意 \(c = 1\) 时,即 \(a_i\) 不需要分裂

此时 \(f_{i, v} = f _ {i + 1, a_i}\) 再加 \(1\),可以理解为多了一个以 \(a_i\) 开头且仅含这个 \(a_i\) 的子段

代码较为直观(因为很短)上面的讲解很史

#include <bits/stdc++.h>

const int MAXN = 100005;
const int MAXV = 100000;
const int MOD  = 998244353;

using namespace std;

struct Node {
	int val, sum;
};

int N, D, V;
int A[MAXN];
uint32_t Ans = 0;

inline void Solve () {
	std::vector <Node> Now, Lst;
	
	cin >> N, Ans = 0;
	
	for (int i = 1; i <= N; ++ i) cin >> A[i];
	
	Lst.push_back ({A[N], 1});
	
	for (int i = N - 1; i; -- i) {
		while (Now.size ()) Lst.push_back (Now.back ()), Now.pop_back ();
		
		for (int l = 1, r; l <= MAXV; l = r + 1) {
			D = (A[i] - 1) / l + 1, V = (D == 1);
			r = (D > 1) ? (A[i] - 1) / (D - 1) : MAXV;
			
			while (Lst.size () && Lst.back ().val <= r) V = (V + Lst.back ().sum) % MOD, Lst.pop_back ();
			
			Ans += (1ll * i * V % MOD * (D - 1) % MOD), Ans = Ans % MOD;
			Now.push_back ({A[i] / D, V});
		}
	}
	
	Now.clear (), Lst.clear ();
	
	cout << Ans << '\n';
}

int T;

int main () {
	
	cin >> T;
	
	while (T --) Solve ();
	
	return 0;
}


Luogu P2260 [清华集训2012] 模积和

推狮子,先容斥一下把 \(i \neq j\) 的部分搞掉

\[\begin {aligned} & \sum _ {i = 1} ^ n { \sum _ {j = 1} ^ m { \left ( n - \left \lfloor \dfrac n i \right \rfloor i \right ) \left ( m - \left \lfloor \dfrac m j \right \rfloor jj \right ) } } & (i \neq j) \\ =& \sum _ {i = 1} ^ n { \left ( n - \left \lfloor \dfrac n i \right \rfloor i \right ) } \times \sum _ {j = 1} ^ m { \left ( m - \left \lfloor \dfrac m j \right \rfloor j \right ) } & (i \neq j) \\ =& \sum _ {i = 1} ^ n { \left ( n - \left \lfloor \dfrac n i \right \rfloor i \right ) } \times \sum _ {j = 1} ^ m { \left ( m - \left \lfloor \dfrac m j \right \rfloor j \right ) } - \sum _ {i = 1} ^ n { \left ( n - \left \lfloor \dfrac n i \right \rfloor i \right ) \left ( m - \left \lfloor \dfrac m i \right \rfloor i \right ) } \\ =& \left ( n ^ 2 - \sum _ {i = 1} ^ n { \left \lfloor \dfrac n i \right \rfloor i } \right ) \times \left ( m ^ 2 - \sum _ {j = 1} ^ m { \left \lfloor \dfrac m j \right \rfloor j } \right ) - \sum _ {i = 1} ^ n { \left ( n - \left \lfloor \dfrac n i \right \rfloor i \right ) \left ( m - \left \lfloor \dfrac m i \right \rfloor i \right ) } \end {aligned} \]

容易发现 前面两部分 数论分块可以秒掉

后面部分一样的拆开,得到

\[\begin {aligned} & \sum _ {i = 1} ^ n { \left ( n - \left \lfloor \dfrac n i \right \rfloor i \right ) \left ( m - \left \lfloor \dfrac m i \right \rfloor i \right ) } \\ =& \sum _ {i = 1} ^ n { \left ( nm - n \left \lfloor \dfrac m i \right \rfloor i - m \left \lfloor \dfrac n i \right \rfloor i + i ^ 2 \left \lfloor \dfrac n i \right \rfloor \left \lfloor \dfrac m i \right \rfloor \right ) } \\ =& n ^ 2 m - \sum _ {i = 1} ^ n { \left ( n \left \lfloor \dfrac m i \right \rfloor i + m \left \lfloor \dfrac n i \right \rfloor i - i ^ 2 \left \lfloor \dfrac n i \right \rfloor \left \lfloor \dfrac m i \right \rfloor \right ) } \end {aligned} \]

然后再次 整除分块 就可以了,注意这是 二维 的(有 \(n, m\) 两个上界)

#include <stdio.h>
#include <stdint.h>

const uint32_t MAXN = 100005;
const uint32_t MOD  = 19940417;
const uint32_t Inv6 = 3323403;

uint32_t N, M, T;
uint32_t d, v;
uint64_t Ans0, Ans1, Ans2, Ans3, Ans;

inline uint64_t Sum1 (const uint64_t L, const uint64_t R) {
	return ((R * (R + 1) >> 1) - (L * (L + 1) >> 1)) % MOD;
}

inline uint64_t Sum2 (const uint64_t L, const uint64_t R) {
	uint64_t Max = R * (R + 1) % MOD * (2 * R + 1) % MOD * Inv6 % MOD;
	uint64_t Min = L * (L + 1) % MOD * (2 * L + 1) % MOD * Inv6 % MOD;
	return (Max + MOD - Min) % MOD;
}



int main () {
	
	scanf ("%d%d", &N, &M);
	
	if (M < N) T = N, N = M, M = T;
	
	for (uint32_t l = 1, r; l <= N; l = r + 1) 
		r = N / (N / l), d = N / l, Ans1 += Sum1 (l - 1, r) * d % MOD; 
	Ans1 = Ans1 % MOD, Ans1 = 1ull * N * N - Ans1, Ans1 = Ans1 % MOD;
	
	for (uint32_t l = 1, r; l <= M; l = r + 1) 
		r = M / (M / l), d = M / l, Ans2 += Sum1 (l - 1, r) * d % MOD;
	Ans2 = Ans2 % MOD, Ans2 = 1ull * M * M - Ans2, Ans2 = Ans2 % MOD;
	
	
	Ans = Ans1 * Ans2 % MOD, Ans1 = Ans2 = 0;
	
	for (uint32_t l = 1, r; l <= N; l = r + 1) {
		r = (M / (M / l)) < (N / (N / l)) ? (M / (M / l)) : (N / (N / l)), d = N / l, v = M / l;
		Ans1 += Sum1 (l - 1, r) * M % MOD * d, Ans1 = Ans1 % MOD; 
		Ans2 += Sum1 (l - 1, r) * N % MOD * v, Ans2 = Ans2 % MOD;
		Ans3 += Sum2 (l - 1, r) * d % MOD * v, Ans3 = Ans3 % MOD;
	}
	
	Ans0 = ((1ull * N * N % MOD * M % MOD) + MOD - Ans1 + MOD - Ans2 + Ans3) % MOD;
	
	printf ("%llu\n", (Ans + MOD - Ans0) % MOD);
	
	return 0;
}

Luogu P3579 [POI2014] PAN-Solar Panels

考虑性质,若 \((a, b]\) 中存在 \(x\)整倍数,则一定有 \(\left \lfloor \dfrac a x \right \rfloor < \left \lfloor \dfrac b x \right \rfloor\)

容易证明 \(\left \lfloor \dfrac b x \right \rfloor\) 至多 \(O (\sqrt V)\) 种取值

考虑枚举取值,在固定 \(b\),固定 \(\left \lfloor \dfrac b x \right \rfloor\) 时,显然 \(x\) 取值在一个区间 \([l, r]\)

显然,\(x\) 越大,\(\left \lfloor \dfrac a x \right \rfloor\) 可能越大,\(\left \lfloor \dfrac a x \right \rfloor < \left \lfloor \dfrac b x \right \rfloor\) 越有可能满足

故满足 \(\left \lfloor \dfrac a x \right \rfloor < \left \lfloor \dfrac b x \right \rfloor\) 等价于在固定 \(\left \lfloor \dfrac b x \right \rfloor\)\(x\) 取值 \([l, r]\) 的情况下,满足 \(\left \lfloor \dfrac a r \right \rfloor < \left \lfloor \dfrac b r \right \rfloor\)

整除分块求解即可,这时候发现有两个区间?二维整除分块 求解即可

#include <stdio.h>
#include <stdint.h>

uint32_t T, a, b, c, d, e;
uint32_t Max;

int main () {
	
	scanf ("%u", &T);
	
	for (uint32_t i = 1; i <= T; ++ i) {
		scanf ("%u%u%u%u", &a, &b, &c, &d);
		Max = 0, -- a, -- c, e = b < d ? b : d;
		for (uint32_t l = 1, r; l <= e; l = r + 1) {
			r = (b / (b / l)) < (d / (d / l)) ? (b / (b / l)) : (d / (d / l));
			if (a / r < b / r && c / r < d / r && r > Max) Max = r;
		}
		printf ("%u\n", Max);
	}
	
	return 0;
}

4. 引用资料

[0] Number Theory —— H_W_Y

posted @ 2024-07-22 20:00  FAKUMARER  阅读(16)  评论(0编辑  收藏  举报