1. 程式人生 > 其它 >數學/數論專題-學習筆記:整除分塊

數學/數論專題-學習筆記:整除分塊

目錄

1. 前言

整除分塊,是一種數論的基礎演算法,必備知識,在很多題目中都有涉及但是題目很基礎。

整除分塊主要解決的是這樣一類問題:求值:

\[\sum_{i=1}^{n}f(i)g(\Big\lfloor\dfrac{k}{i}\Big\rfloor) \]

其中已知 \(f\) 的字首和或者能 \(O(1)\) 計算 \(f(r)-f(l)\)

正常的計算方式是 \(O(n)\) 的,但是一些題裡面該複雜度不能接受,此時就需要整除分塊,整除分塊能做到 \(O(\sqrt{n})\) 解決此類問題。

2. 詳解

首先討論這個問題的簡化版本:

\[\sum_{i=1}^{n}\Big\lfloor\dfrac{k}{i}\Big\rfloor \]

通過打表可以發現,對於 \(\Big\lfloor\dfrac{k}{i}\Big\rfloor\),容易有一個較大的區間 \([l,r]\),滿足 \(\forall i,j \in [l,r],\Big\lfloor\dfrac{k}{i}\Big\rfloor=\Big\lfloor\dfrac{k}{j}\Big\rfloor\),下面定義一個滿足上述條件的區間的值為 \(i \in [l,r],\Big\lfloor\dfrac{k}{i}\Big\rfloor\)

也就是說有連續段是相同的,因此整除分塊需要考慮如何快速處理出這些區間,最好的方式當然就是將區間處理完之後,不同的區間的值不同。

其實數學上已經證明了一些東西,這裡不進行證明,直接將結論拉過來用:

  1. 這樣的區間只有 \(O(\sqrt{n})\) 個。
  2. 對於一個區間 \([l,r]\),滿足 \(r=\Big\lfloor\dfrac{k}{\lfloor\frac{k}{i}\rfloor}\Big\rfloor\)

因此我們可以考慮從 \(l=1\) 開始,根據上述結論 2 算出 \(r\),而 \([l,r]\) 的值顯然是 \((r-l+1) \times \Big\lfloor\dfrac{k}{l}\Big\rfloor\),然後 \(l\leftarrow r+1\),又根據結論 1,複雜度為 \(O(\sqrt{n})\)

解決這個問題之後,回到我們原本的問題:

\[\sum_{i=1}^{n}f(i)g(\Big\lfloor\dfrac{k}{i}\Big\rfloor) \]

同樣的,我們考慮處理出所有 \([l,r]\),滿足 \(\forall i,j \in [l,r],\Big\lfloor\dfrac{k}{i}\Big\rfloor=\Big\lfloor\dfrac{k}{j}\Big\rfloor\),下面定義 \([l,r]\) 的值是 \(\sum_{i=l}^{r}f(i)g(\Big\lfloor\dfrac{k}{i}\Big\rfloor)\)

這裡先將這些區間存下來,記作 \([l_i,r_i]\),然後我們注意到此時的 \(\Big\lfloor\dfrac{k}{i}\Big\rfloor\) 是一個常數,可以直接將 \(g(\Big\lfloor\dfrac{k}{i}\Big\rfloor)\) 提出去,此時 \([l_i,r_i]\) 對答案的貢獻就變成了這樣:

\[g(\Big\lfloor\dfrac{k}{l_i}\Big\rfloor)\sum_{i=l_i}^{r_i}f(i) \]

然後因為可以快速計算 \(f(r)-f(l)\),所以照樣可以 \(O(\sqrt{n})\) 解決。

程式碼裡面注意兩個點:

  1. 大於 \(n\) 的區間右端點要變成 \(n\)
  2. \(\Big\lfloor\dfrac{k}{l}\Big\rfloor\) 為 0 的時候 \(r=n\)

通用 Code:

n = Read(), k = Read(); ans = 0;
for (int l = 1, r; l <= n; l = r + 1)
{
	if (k / l == 0) r = n;
	else r = Min(k / (k / l), n);
	++cnt; Left[cnt] = l, Right[cnt] = r;
}
for (int i = 1; i <= cnt; ++i)
	ans += (f(Left[i], Right[i]) * g(k / Left[i]));

其中 \(f(l,r)\) 計算 \(\sum_{i=l}^{r}f_i\)\(g(i)\) 就是算 \(g(i)\)

接下來看道例題:P2261 [CQOI2007]餘數求和

這道題給出 \(n,k\),要求:

\[\sum_{i=1}^{n}k \bmod i \]

首先將 \(\bmod\) 拆掉,將式子變成這樣:

\[\sum_{i=1}^{n}(k-i \times \Big\lfloor\dfrac{k}{i}\Big\rfloor) \]

然後發現這個 \(k\) 可以提出去,變成這樣:

\[nk-\sum_{i=1}^{n}(i \times \Big\lfloor\dfrac{k}{i}\Big\rfloor) \]

我們只需要處理後面這個式子,對應到整除分塊上,發現 \(f(i)=i,g(\Big\lfloor\dfrac{k}{i}\Big\rfloor)=\Big\lfloor\dfrac{k}{i}\Big\rfloor\),滿足 \(O(1)\) 計算 \(f(r)-f(l)\) 的條件,因此可以整除分塊求後面這個式子,做完了。

Code:

/*
========= Plozia =========
	Author:Plozia
	Problem:P2261 [CQOI2007]餘數求和
	Date:2022/2/18
========= Plozia =========
*/

#include <bits/stdc++.h>

typedef long long LL;
const int MAXN = 1e5 + 5;
int n, k, Left[MAXN], Right[MAXN], cnt;
LL ans = 0;

int Read()
{
	int sum = 0, fh = 1; char ch = getchar();
	for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
	for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = sum * 10 + (ch ^ 48);
	return sum * fh;
}
int Max(int fir, int sec) { return (fir > sec) ? fir : sec; }
int Min(int fir, int sec) { return (fir < sec) ? fir : sec; }

int main()
{
	n = Read(), k = Read(); ans = 1ll * n  * k;
	for (int l = 1, r; l <= n; l = r + 1)
	{
		if (k / l == 0) r = n;
		else r = Min(k / (k / l), n);
		++cnt; Left[cnt] = l, Right[cnt] = r;
	}
	for (int i = 1; i <= cnt; ++i)
		ans = ans - 1ll * (k / Left[i]) * 1ll * (Left[i] + Right[i]) * (Right[i] - Left[i] + 1) / 2;
		// 這裡通過直接推式子代替了 f 和 g 函式
	printf("%lld\n", ans); return 0;
}

3. 總結

整除分塊將形如 \(\sum_{i=1}^{n}f(i)g(\Big\lfloor\dfrac{k}{i}\Big\rfloor)\) 的式子化為了多個形如 \(g(\Big\lfloor\dfrac{k}{l_i}\Big\rfloor)\sum_{i=l_i}^{r_i}f(i)\) 的式子,其中 \(f(r)-f(l)\) 能夠快速計算,從而減少時間複雜度,降至 \(O(\sqrt{n})\)

4. 參考資料