min 25 模板使用說明

22 minute read

min 25 模板使用說明

前幾個月(WF 之前)忘記為什麼就想去學 min25 篩,然後就把他模版化放進 codebook 裡了。我是看 https://oi-wiki.org/math/number-theory/min-25/ 學的,不過本文不會提到太多細節。

先貼上 code:

 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
30
31
32
33
34
35
36
template <typename U, typename V> struct min25 {
  lld n; int sq;
  vector<U> Ss, Sl, Spre; vector<V> Rs, Rl;
  Sieve sv; vector<lld> quo;
  U &S(lld d) { return d < sq ? Ss[d] : Sl[n / d]; }
  V &R(lld d) { return d < sq ? Rs[d] : Rl[n / d]; }
  min25(lld n_) : n(n_), sq((int)sqrt(n) + 1),
    Ss(sq), Sl(sq), Spre(sq), Rs(sq), Rl(sq), sv(sq) {
    for (lld i = 1, Q; i <= n; i = n / Q + 1)
      quo.push_back(Q = n / i);
  }
  U F_prime(auto &&f, auto &&F) {
    for (lld p : sv.primes) Spre[p] = f(p);
    for (int i = 1; i < sq; i++) Spre[i] += Spre[i - 1];
    for (lld i : quo) S(i) = F(i) - F(1);
    for (lld p : sv.primes)
      for (lld i : quo) {
        if (p * p > i) break;
        S(i) -= f(p) * (S(i / p) - Spre[p - 1]);
      }
    return S(n);
  } // F_prime: \sum _ {p is prime, p <= n} f(p)
  V F_comp(auto &&g, auto &&h) {
    for (lld i : quo) R(i) = h(S(i));
    for (lld p : sv.primes | views::reverse)
      for (lld i : quo) {
        if (p * p > i) break;
        lld prod = p;
        for (int c = 1; prod * p <= i; ++c, prod *= p) {
          R(i) += g(p, c) * (R(i / prod) - h(Spre[p]));
          R(i) += g(p, c + 1);
        }
      }
    return R(n);
  } // F_comp: \sum _ {2 <= i <= n} g(i)
}; // O(n^{3/4} / log n)

如果去看我們 codebook 的連結 會看到還有一串中文的註解,以下大概就是仔細的講那一串中文的註解然後隨機補充一些東西。

Description

這個模板要解決的問題簡單來說就是有一個積性函數 gg,計算 ∑i≤ng(i)\sum _ {i \leq n} g(i)。因為是積性函數,所以只要給定質數冪的值,即 g(pc)g(p^c),就可以對所有正整數定義 gg 的值。然而為了快速計算 gg 還必須滿足一些條件,在學 min25 篩時常常會看到「質數點 pp 必須是一個低階多項式」,不過其實可以更一般化。使用這個模板時必須要構造兩個 type U,VU, V 以及函數 f,hf, h 滿足以下條件:

  1. U,VU, V 都是環,即在上面可以定義加法、乘法,加法可交換且有反元素(可以做減法),乘法對加法有分配律。
  2. 要求 f:N→Uf: \mathbb{N} \to U 是完全積性,即 ∀a,b∈N,f(ab)=f(a)f(b)\forall a, b \in\mathbb{N}, f(ab) = f(a)f(b)。
  3. ff 的前綴和 F(i)=∑j≤if(i)F(i) = \sum _ {j \leq i} f(i) 可以快速求值。
  4. 對於質數 pp,g(p)=h(f(p))g(p) = h(f(p))。
  5. h:U→Vh: U \to V 是一個加法上的同態,即 h(u1+u2)=h(u1)+h(u2)h(u_1 + u_2) = h(u_1) + h(u_2)。
  6. 給定 p,cp, c 之後,g(pc)g(p^c) 可以快速求值。

呼叫 F_prime 會對所有 x∈Dn=⌊ni⌋∣i=1,2,…,nx \in D_n = \\{\lfloor \frac{n}{i} \rfloor | i=1,2,\dots,n \\} 計算 S(x)=∑p is prime,p≤xf(p)S(x) = \sum _ {p \text{ is prime}, p \leq x} f(p)。請注意 F(x)=∑i≤xf(i)F(x) = \sum _ {i \leq x} f(i),和 SS 不同。
呼叫 F_comp 會對所有 x∈Dnx \in D_n 計算 R(x)=∑2≤i≤xg(i)R(x) = \sum _ {2 \leq i \leq x} g(i)。函數回傳的值就是 R(n)R(n),也就是最一開始求的值。注意這個值不會把 g(1)g(1) 算進去。

以下舉幾個例題說明。

sum of totient function

大家很熟悉的積性函數 φ(n)\varphi(n)。對於質數冪,φ(pc)=pc−1(p−1)\varphi(p^c) = p^{c-1}(p-1),特別的,對於質數 pp 有 φ(p)=p−1\varphi(p) = p - 1。這滿足所謂低階多項式的條件。讓我們構造一下 ff

{f(n)=(1,n)h((s0,s1))=s1−s0 \begin{cases} f(n) &= (1, n) \\ h((s_0, s_1)) &= s_1 - s_0 \end{cases}

其中,U=Z2U = \mathbb{Z}^2 上的加法跟乘法分別都定義成逐項加跟逐項乘,即 (a,b)+(c,d)=(a+c,b+d)(a, b) + (c, d) = (a + c, b + d) 跟 (a,b)⋅(c,d)=(ac,bd)(a, b) \cdot (c, d) = (ac, bd)。可以驗證對於質數 pp,h(f(p))=h((1,p))=p−1=φ(p)h(f(p)) = h((1, p)) = p - 1 = \varphi(p)。

在 g(p)=∑aipig(p) = \sum a_i p^i 的時候,UU 的構造基本上就是一個 tuple 維護 n0,n1,n2,…n^0, n^1, n^2, \dots,而 idk:n↦nk\text{id}_k: n\mapsto n^k 正是完全積性並且前綴和很好算的函數。hh 則就是把這些冪次用 aia_i 線性組合得到 gg。

sum of multiplicative function

和 sum of totient function 幾乎一樣,只是 hh 乘的系數稍微改一下。稍微放一點片段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    min25<Pair, Mint> m(N);
    m.F_prime([](Mint p) -> Pair {
      return {1, p};
    }, [](Mint n) -> Pair {
      return {n, n * (n + 1) / 2};
    });

    Mint ans = 1 + m.F_comp([&](Mint p, Mint e) -> Mint {
      return a * e + b * p;
    }, [&](Pair v) -> Mint {
      return a * v[0] + b * v[1];
    });

其中 Pair 是一個定義了加法乘法減法等等的 struct,也可以直接用 std::valarray 代替,但可能會跑比較慢。

可以看 這個 submission 或是 另一個 submission。

counting primes

可以只呼叫 F_prime 得到質數的個數,此時 f(n)=1f(n) = 1 以及 F(n)=nF(n) = n。
可見 https://judge.yosupo.jp/submission/255568 。

LOJ #6053. 简单的函数

https://loj.ac/p/6053

本題的 g(pc)=p⊕cg(p^c) = p \oplus c。因為質數除了 22 以外都是奇數,所以 g(p)=p−1g(p) = p - 1,除了 g(2)=3g(2) = 3。其中一個構造如下:

{f(n)=(1,n,[n is odd])h((s0,s1,sparity))=s1+s0−2sparity \begin{cases} f(n) &= (1, n, [n \text{ is odd}]) \\ h((s_0, s_1, s_{parity})) &= s_1 + s_0 - 2 s_{parity} \end{cases}

注意到 n↦[n is odd]n \mapsto [n \text{ is odd}] 也是一個完全積性函數,並且其前綴和很好計算:介在 [1,n][1, n] 的奇數數量正好就是 ⌈n2⌉\lceil \frac{n}{2} \rceil。這裡 U=Z3U = \mathbb{Z}^3 並且類似的定義加法乘法。

QOJ # 9867. Flowers

這是在 3rd ucup kunming 遇到的題目。原題有一些包裝,但總之就是要計算 ∏i≤nω(i)\prod _ {i \leq n} \omega(i),其中 ω(i)\omega(i) 是 ii 有幾種相異的質因數。因為 n≤1011n\leq 10^{11},用 python 打一下可以知道 ω(i)≤10\omega(i) \leq 10。

因為相異質因數個數很小,可以打算對於所有 k=1,…,10k = 1,\dots,10 計算有幾個 ii 使得 ω(i)=k\omega(i) = k 之後再快速冪計算原題要求的答案。為了數有幾個相異質因數,構造 V=Z[x]V = \mathbb{Z}[x] 為多項式,而 g(pc)=xg(p^c) = x,這樣當 ii 有 kk 個相異質因數,g(i)g(i) 就會等於 xkx^k,而 ∑2≤i≤ng(i)\sum _ {2\leq i \leq n} g(i) 就是我們想要統計的東西。

接著想要構造一個前綴和很好算的 ff 使得在質數點 ff 和 gg 可以通過 hh 轉換。從 ∑p∈P≤ng(p)=π(n)x\sum _ {p\in\mathbb{P} _ {\leq n}} g(p) = \pi(n) x 可以看出(hint: ∑p∈P≤ng(p)=∑p∈P≤nh(f(p))=h(∑p∈P≤nf(p))\sum _ {p\in\mathbb{P} _ {\leq n}} g(p) = \sum _ {p\in\mathbb{P} _ {\leq n}} h(f(p)) = h\left( \sum _ {p\in\mathbb{P} _ {\leq n}} f(p) \right),不限於此題)在 F_prime 階段要計算的大概就會是質數數量,不妨構造 f(n)=1f(n) = 1 以及 h(s)=sxh(s) = sx。

因為 ω(i)≤10\omega(i) \leq 10,所以本題的多項式 degree 也小於等於 1010,因此我的實做是直接暴力 10×1010\times 10 寫乘法,但實際上也可以用 NTT,或甚至在一開始任選 11 個數字,然後在 min25 篩計算他們帶入多項式的點值的和,最後再用拉格朗日插值或高斯消去等方法得出答案的多項式,不過嫌麻煩就沒有把這個 10 壓掉也是過了。


另外一點我想提的是 F_comp 有另外一個寫法是用 top-down dfs 甚至不記憶化 的寫法去計算,應該是 OI-wiki 上的所謂 O(n1−ε)\mathcal{O}(n^{1-\varepsilon}) 的作法,大概跟 F_comp 想做的事情差不多。把 F_comp 滾動掉的那個維度 explicit 的寫出來就會變成下面這樣:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  V F_comp_dfs(auto &&g, auto &&h) {
    auto dfs = [&](auto &&self, size_t k, lld x) -> V {
      V ans = h(S(x));
      for (size_t i = k; i < sv.primes.size(); i++) {
        const lld p = sv.primes[i];
        if (p * p > x) break;
        lld prod = p;
        for (int c = 1; prod * p <= x; ++c, prod *= p) {
          ans += g(p, c) * (self(self, i + 1, x / prod) - h(Spre[p]));
          ans += g(p, c + 1);
        }
      }
      return ans;
    };
    return dfs(dfs, 0, n);
  }

節錄自 這個 submission。本文的這個 O(n34log⁡−1n)\mathcal{O}(n^{\frac{3}{4}} \log ^{-1} n) 複雜度好像會在中國被分類成洲閣篩?而 dfs 的寫法才被叫做 min25 篩。複雜度雖然是 O(n1−ε)\mathcal{O}(n^{1-\varepsilon}) 但在競程範圍內算是蠻快的,如果 F_comp 特別慢的題目,例如 QOJ 那題,會有顯著加速。

Time Complexity

O(n34/log⁡n)\mathcal{O}(n^{\frac{3}{4}} / \log n)。從程式碼裡面就可以看出來(注意到 quo 是從大到小枚舉) F_prime 的複雜度是

∑p is prime,p≤n∑x∈Dn[p2≤x]=∑x∈Dnπ(x)≤∑i2≤nπ(i)+∑i2≤nπ(ni)≈∑i2≤niln⁡i+∑i2≤nniln⁡ni \sum _ {p \text{ is prime}, p \leq \sqrt{n}} \sum _ {x \in D_n} [p^2 \leq x] = \sum _ {x\in D_n} \pi(\sqrt{x}) \leq \sum _ {i^2 \leq n} \pi(\sqrt{i}) + \sum _ {i^2 \leq n} \pi(\sqrt{\frac{n}{i}}) \approx \sum _ {i^2 \leq n} \frac{\sqrt{i}}{\ln{\sqrt{i}}} + \sum _ {i^2 \leq n} \frac{\sqrt{\frac{n}{i}}}{\ln{\sqrt{\frac{n}{i}}}}

後續就懶得證了。應該可以在隨意的 ncn^c 切一刀,一半把分母當成 11 積分也只會是 O(n34−ε)O(n^{\frac{3}{4}-\varepsilon}),另一半則是可以把 ln⁡(⋯ )\ln(\cdots) 當成 log⁡(n)\log(n) 的常數倍。F_comp 的複雜度應該也可以證明是一樣的。

空間跟 DnD_n 集合的大小一樣是 O(n)\mathcal{O}(\sqrt{n})。

Caveats

這份程式碼並沒有直接要求 U,VU, V 的加法單位元和乘法單位元(0,10, 1),但都預設空的 vector 會是 00,並且因為 f,gf, g 是積性的,定義上應該要滿足 f(1)=1U,g(1)=1Vf(1) = 1_U, g(1) = 1_V。

另外,完全積性函數這個條件,我懷疑其實也是可以拿掉的。在第一階段(F_prime)我們會把包含小質數的數字全部扣掉,這相當於是跟 f−1f^{-1} 做捲積,而當 ff 是完全積性函數,inverse 會非常好算:fp−1(x)=1−f(p)xf_p^{-1}(x) = 1 - f(p)x(見貝爾級數),若 ff 不是完全積性的,inverse 要花點力氣算。不過,我們仍然會需要構造一個前綴和很好算的積性函數 ff,這是這個演算法的大前提。把完全積性的條件改掉之後可能常數會差一點,而且當初寫的時候沒有想這麼多。

這份程式碼如果 f,g,h,Ff,g,h,F 都是 O(1)\mathcal{O}(1) 的話,大概可以跑 101010^{10} 到 101110^{11}。因為據說有 O~(N)\tilde{\mathcal{O}}(\sqrt{N}) 的演算法了所以就去翻了一堆文章,還是連同樣條件下的 O(n23log⁡−1n)\mathcal{O}(n^{\frac{2}{3}} \log^{-1}n) 的版本也寫不出來。去翻 library checker 的各種 top coder 好像也都是 O~(n23)\tilde{\mathcal{O}}(n^{\frac{2}{3}}) 的複雜度的。只有再次學會了杜教篩,寫了一個帶 template 的模板過了 sum_of_totient_function,但跟本文的模板比也只是 192ms 跟 397ms 的差距而已。