數學/數論專題-學習筆記:整除分塊
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. 詳解
首先討論這個問題的簡化版本:
通過打表可以發現,對於 \(\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\)。
也就是說有連續段是相同的,因此整除分塊需要考慮如何快速處理出這些區間,最好的方式當然就是將區間處理完之後,不同的區間的值不同。
其實數學上已經證明了一些東西,這裡不進行證明,直接將結論拉過來用:
- 這樣的區間只有 \(O(\sqrt{n})\) 個。
- 對於一個區間 \([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})\) 解決。
程式碼裡面注意兩個點:
- 大於 \(n\) 的區間右端點要變成 \(n\)。
- 當 \(\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})\)。