Min_25篩 代碼及模板

LOJ簡單的函數

以這道題爲例子,講一下min_25篩如何用代碼實現。
首先min_25是一種亞線性篩,可以處理1e9以上的數據,老版min_25篩的複雜度爲O(n0.75logn)O(\frac{n^{0.75}}{logn}),新版已經優化到O(n23)O(n^{\frac{2}{3}}),複雜度,我也不會證明。
據說常數比較大,確實是跑得快,但不過差距也不是特別大,我們還是說老版min_25篩吧。
min_25算法原理
上面這個博客寫得比較簡單,而且好理解,建議先大概看懂上面那個博客在看其他博客。
如果有錯誤,懇請各位指出來!
首先我先把min25的幾部分擺出來吧,min25就是難在質數求和。

首先是運用的要求

  • 首先對於積性函數f(x)f(x),當xprimex\in prime時,f(x)f(x)是一個關於xx的簡單多項式,簡單來說就是項數不能太多,冪次也不能太高,等會兒實現過程你就會發現。
  • f(pk)pprimef(p^{k}),p \in prime能夠快速求得,這裏快速求得,起碼應該是log級別吧,等會兒代碼實現過程你就會發現了。

我們發現其實對於一個積性函數來說,其實我們見過第一個條件很多積性函數都滿足。
比如μ(x)\mu(x),發現當爲質數時,等於-1,而且第二個條件也很容易知道滿足,所以莫比烏斯函數,可以用min25篩求他的前綴和。
同理,對於歐拉函數φ(x)\varphi(x),當爲質數時,等與p1p-1,第二個條件φ(pk)=pk1(p1)\varphi(p^{k})=p^{k-1}(p-1),同樣也可以用min25篩。
再比如求i=0x1gcd(x,i)μ(i)\sum_{i=0}^{x-1} gcd(x,i)\mu(i),這個前綴和。總的來說很多積性函數的前綴和都可以秒殺。
我們看題f(1)=1,f(pc)=pc,f(ab)=f(a)f(b)[gcd(a,b)==1]f(1)=1,f(p^c)=p\bigoplus c,f(ab)=f(a)f(b)[gcd(a,b)==1]

首先我們可以發現當p=2p=2f(2)=3f(2)=3,其他質數時f(p)=p1f(p)=p-1,即f(p)=p1+2[p==2]f(p)=p-1+2[p==2],可以認爲是多項式。同時第二個條件也滿足。

接下講一下第一部分,求g函數、

首先要處理關於質數的值,因爲積性函數質數點的值是關於質數的多項式,我們接下來的假設是有意義的。

  • 首先我們設一個函數 g(n,j)=i=1nik,(iprime)or(min(p)pj,pi,pprime)g(n,j)=\sum_{i=1}^{n} i^k,(i\in prime) or(min(p)\geq p_{j},p|i,p\in prime)換成中文就是,ii爲質數,或者ii的最小質因子大於pjp_{j}pjp_j表示第jj個質數。
  • 然後我們發現上面的每一個nnjj都對應了一個狀態,我們考慮一些動態規劃的思想,找一找這個式子的遞推方程 。 我們考慮從g(n,j1)g(n,j)g(n,j-1)\to g(n,j),當pj2>np_j^2>n時,表面在11nn的範圍內,不存在最小質因子爲pjp_j的數,因此對於總和沒有貢獻,即g(n,j)=g(n,j1),pj2>ng(n,j)=g(n,j-1),p_j^2>n時。
  • 接下來考慮pj2np_j^2\leq n,這時從 g(n,j1)g(n,j)g(n,j-1)\to g(n,j),明顯符合條件的數減少了,因此需要減去xxg(n,j)=g(n,j1)xg(n,j)=g(n,j-1)-x,我們現在考慮一下,這個xx顯然應該是最小質因子是pjp_{j}的一部分的值,我們減去最小質因子大於等於pjp_j的那一部分值,我們不能從g(n,j1)g(n,j-1)出發,這時,我們注意到,iki^k是一個完成積性函數,因此我們可以這樣狀態轉義,減去這一部分pjkg(npj,j1)p_j^{k}g(\frac{n}{p_j},j-1)表示所有含有pjp_j的值,因爲這裏多減去了質數的部分的,所以要加上這一部分的值pjkg(pj1,j1)p_j^kg(p_{j-1},j-1),相當於一個容斥,如果不能很好的理解,可以手推模擬一下。
  • 狀態轉移方程g(n,j)={g(n,j1),pj2>ng(n,j1)pjk[g(npj,j1)g(pj1,j1)],pj2ng(n,j)=\begin{cases}g(n,j-1), p_j^2>n \\g(n,j-1)-p_j^k[g(\frac{n}{p_j},j-1)-g(p_{j-1},j-1)], p_j^2\leq n\end{cases}

我們顯然可以知道g(pj,j)=i=1jpjkg(p_j,j)=\sum_{i=1}^{j}p_j^k,這裏我們用p|p|表示質數集的大小,因此g(n,p)g(n,|p|)就表示範圍滿足條件的質數的和,因此這裏的質數我們,篩到n\sqrt n就可以了,同時g(n,0)=i=2nikg(n,0)=\sum_{i=2}^{n}i^k,注意沒有1哦。
其實這個一個過程就很像埃篩的思想,先篩出前j1j-1個質數的情況,在推出jj的情況。
這裏我們上面要求f(p)f(p)是關於pp的簡單多項式,就是我們在求gg函數的時候方便求出來而已。我們來看看代碼具體如何實現。

  1. 首先我們可以看到上面的遞推,我們可以發現題目給出n的範圍一般很大的,直接開數組是開不下的,但不過因爲轉移的時候只和整除有關,所以我們在代碼實現時考慮數論分塊後的值,這樣的值大約有2n2\sqrt n個。
  2. 我們首先要遞推gg函數,因此要確定gg的邊界,即g(n,0)=i=2nf(i)=i=2naxixg(n,0)=\sum_{i=2}^{n}f(i)=\sum_{i=2}^{n}\sum a_{x}i^{x},注意沒有包括1,其實1包不包括不重要,最後加上就是了,記住這裏的f(i)f(i)是假的,就是把f(i)f(i)當成全部是質數的情況來計算,纔有後面的一部分,因爲f(p)f(p)是多項式,因此少不了自然數冪求和,以及拉格朗日插值等等,所以就和上面的對應起來了,如果複雜了,肯定不好求,而且效率堪憂,建議自然數的冪次不要超過100,否則插值,會花費大量時間。
  3. 接下來就是正常的類似動態規劃的遞推了,注意應該在上一步中處理好整除分塊後的值。
int getid(ll x) {
    if (x <= sqr)
        return id1[x];
    return id2[n / x];
}//sqr=sqrt(n);
void calc1() {
    for (ll l = 1, r; l <= n; l = r + 1) {
        r = n / (n / l), w[++m] = n / l;//假設全部是質數。
        h[m] = (w[m] - 1) % mod;//常數項
        g[m] = (w[m] % mod) * ((w[m] + 1) % mod) % mod;//一次項
        g[m] *= inv2, g[m]--;
        if (w[m] <= sqr)
            id1[w[m]] = m;
        else
            id2[r] = m;
    }
    for (int j = 1; j <= cnt; j++) {
        for (int i = 1; i <= m && pri[j] * pri[j] <= w[i]; i++) {
            int k = getid(w[i] / pri[j]);
            (g[i] -= 1ll * pri[j] * (g[k] - pre[j - 1]) % mod) %= mod;
            (h[i] -= h[k] - j + 1) %= mod;
        }
    }
}

時間複雜的的證明…我不會。。。。看這裏吧時間複雜度

怎麼求和

說到這裏到底該怎麼求和呢。。。

  • 我們還是設一個函數s(n,j)=i=2nf(i)[min(p)pj,pi,pprime]s(n,j)=\sum_{i=2}^{n}f(i)[min(p) \geq p_j,p|i,p\in prime],中文就是ii的最小質因子大於等於pjp_j注意這裏和上面不同是大於等於,不是大於了,並且沒有質數的必須計算的條件。
  • 下面我們來思考,如何利用gg函數來輔助遞推ss函數。我可以把ss分成兩部分,一部分是質數的答案,一部分是合數的答案,關於質數部分的答案我們就可以了利用gg函數來遞推了即g(n,p)i=1j1f(pi)g(n,|p|)- \sum_{i=1}^{j-1}f(p_i).這是質數部分的。
  • 下面來考慮合數部分的值了。首先我們吧合數的最小質因子提出來,這些質因子都是大於等於pjp_j的,可以得到k=jpe=1pke+1n[f(pke)s(npke,k+1)+f(pke+1)]\sum_{k=j}^{|p|}\sum_{e=1}^{p_{k}^{e+1}\leq n}[f(p_{k}^{e})s(\frac{n}{p_{k}^{e}},k+1)+f(p_{k}^{e+1})]爲什麼這樣就可以了?因爲f(i)f(i)是一個積性函數因此把最小質因子全部提出來然後相乘是肯定沒有問題,因此我們可以枚舉最小質因子以及冪次,就可以了,後面的那個一尾巴,是因爲在處理過程種所以的f(pki)f(p_{k}^{i})都被篩掉了,因此要補充進來。
  • 因此s(n,j)=g(n,p)i=1j1f(pi)+k=jpe=1pke+1n[f(pke)s(npke,k+1)+f(pke+1)]s(n,j)=g(n,|p|)- \sum_{i=1}^{j-1}f(p_i)+\sum_{k=j}^{|p|}\sum_{e=1}^{p_{k}^{e+1}\leq n}[f(p_{k}^{e})s(\frac{n}{p_{k}^{e}},k+1)+f(p_{k}^{e+1})],我們就可以利用遞歸進行狀態轉移了。同時從上面也可以看出來爲什麼要求條件二成立了。

代碼實現如下,我可以利用dfs實現狀態轉移,剩下的就是按照公式來就是了。那麼我們要求的就是s(n,1)s(n,1)最後加上1對應的函數值就可以了。

ll solve(ll x, int y) {
    if (x <= 1 || pri[y] > x)
        return 0;//邊界條件
    int k = getid(x);//同樣注意分塊
    ll ret = (g[k] - pre[y - 1] - h[k] + y - 1) % mod;
    if (y == 1)
        ret += 2;//注意2的特殊性
    for (int i = y; i <= cnt && 1ll * pri[i] * pri[i] <= x; i++) {
        ll t1 = pri[i], t2 = 1ll * pri[i] * pri[i];
        for (int e = 1; t2 <= x; ++e, t1 = t2, t2 *= pri[i]) {
            (ret += ((1ll * solve(x / t1, i + 1) * (pri[i] ^ e) % mod + (pri[i] ^ (e + 1)) % mod))) %= mod;
        }
    }
    return ret;
}

好了LOJ6053已經講解完了。
總結一下

  • 首先看函數滿不滿足條件。
  • 其次找f(p)f(p),然後分開項計算gg函數。
  • 然後計算ss函數

這應該是標準模板吧。
然後min25還可以處理特殊質因子,最大質因子,次大質因子,最小質因子等問題。。。
感覺遲早要完。。。
下面來一道,洛谷上面的模板題
https://www.luogu.com.cn/problem/P5325

#include "bits/stdc++.h"

using namespace std;
inline int read() {
    int x = 0;
    bool f = 1;
    char c = getchar();
    for (; !isdigit(c); c = getchar()) if (c == '-') f = 0;
    for (; isdigit(c); c = getchar()) x = (x << 3) + (x << 1) + c - '0';
    if (f) return x;
    return 0 - x;
}
typedef long long ll;
const int maxn = 100010;
const ll mod = 1000000000 + 7;
ll quick(ll a, ll n, ll p) {
    ll ans = 1;
    for (; n; n >>= 1, a = a * a % p)
        if (n & 1) ans = ans * a % p;
    return ans;
}
int vis[maxn], sqr, cnt = 0;
ll suf1[maxn], suf2[maxn], inv2, inv6, n, pri[maxn];
void init(int lim) {
    vis[1] = 1;
    for (int i = 2; i <= lim; i++) {
        if (!vis[i]) {
            pri[++cnt] = i;
            suf1[cnt] = suf1[cnt - 1] + i;
            suf2[cnt] = (suf2[cnt - 1] + 1ll * i * i % mod) % mod;
        }
        for (int j = 1; j <= cnt && i * pri[j] <= lim; j++) {
            vis[i * pri[j]] = 1;
            if (i % pri[j] == 0) break;
        }
    }
}
int id1[maxn], id2[maxn], m = 0;
ll w[maxn << 1], g[maxn << 1], h[maxn << 1];
int getid(ll x) {
    if (x <= sqr) return id1[x];
    return id2[n / x];
}
ll sum1(ll x) {
    x %= mod;
    return (x + 1) * x % mod * inv2 % mod;
}
ll sum2(ll x) {
    x %= mod;
    return x * (x + 1) % mod * (2ll * x % mod + 1) % mod * inv6 % mod;
}
void calc1() {
    for (ll l = 1, r; l <= n; l = r + 1) {
        r = n / (n / l);
        w[++m] = n / l;
        g[m] = sum2(w[m]) - 1;
        h[m] = sum1(w[m]) - 1;
        if (w[m] <= sqr) id1[w[m]] = m;
        else id2[r] = m;
    }
    for (int j = 1; j <= cnt; j++) {
        for (int i = 1; i <= m && pri[j] * pri[j] <= w[i]; i++) {
            int k = getid(w[i] / pri[j]);
            (g[i] -= pri[j] * pri[j] % mod * (g[k] - suf2[j - 1]) % mod) %= mod;
            (h[i] -= pri[j] * (h[k] - suf1[j - 1]) % mod) %= mod;
        }
    }
}
ll solve(ll x, int y) {
    if (x <= 1 || pri[y] > x) return 0;
    int k = getid(x);
    ll ret = (g[k] - suf2[y - 1] - h[k] + suf1[y - 1]) % mod;
    for (int i = y; i <= cnt && pri[i] * pri[i] <= x; i++) {
        ll t1 = pri[i], t2 = pri[i] * pri[i];
        for (int e = 1; t2 <= x; ++e, t1 = t2, t2 *= pri[i]) {
            ll pk = quick(pri[i], e, mod);
            (ret += pk * (pk - 1) % mod * solve(x / t1, i + 1) % mod) %= mod;
            pk = pk * pri[i] % mod;
            (ret += pk * (pk - 1) % mod) %= mod;
        }
    }
    return ret;
}
int main() {
    inv2 = quick(2, mod - 2, mod);
    inv6 = quick(6, mod - 2, mod);
    cin >> n;
    sqr = sqrt(n);
    init(sqr);
    calc1();
    ll ans = solve(n, 1) + 1;
    cout << (ans + mod) % mod << endl;
    return 0;
}

過年在家無事(太墮落了。。)無事於是想了想和杜教篩比一比速度。。。。
就拿洛谷上面那一道杜教篩模板題來對比一下


上面min25下面杜教篩。。
看來差距不是很大,杜教篩稍優

#include "bits/stdc++.h"

using namespace std;
inline int read() {
    int x = 0;
    bool f = 1;
    char c = getchar();
    for (; !isdigit(c); c = getchar()) if (c == '-') f = 0;
    for (; isdigit(c); c = getchar()) x = (x << 3) + (x << 1) + c - '0';
    if (f) return x;
    return 0 - x;
}
typedef long long ll;
const int maxn = 100010;
const ll mod = 1000000000 + 7;
ll quick(ll a, ll n) {
    ll ans = 1;
    for (; n; n >>= 1, a = a * a)
        if (n & 1) ans = ans * a;
    return ans;
}
int vis[maxn], sqr, cnt = 0;
ll suf1[maxn], suf2[maxn], inv2, inv6, n, pri[maxn];
void init(int lim) {
    vis[1] = 1;
    for (int i = 2; i <= lim; i++) {
        if (!vis[i]) {
            pri[++cnt] = i;
            suf1[cnt] = suf1[cnt - 1] + i;
        }
        for (int j = 1; j <= cnt && i * pri[j] <= lim; j++) {
            vis[i * pri[j]] = 1;
            if (i % pri[j] == 0) break;
        }
    }
}
int id1[maxn], id2[maxn], m = 0;
ll w[maxn << 1], g[maxn << 1], h[maxn << 1];
int getid(ll x) {
    if (x <= sqr) return id1[x];
    return id2[n / x];
}

ll sum2(ll x) {
    return x * (x + 1) / 2;
}

void calc1() {
    for (ll l = 1, r; l <= n; l = r + 1) {
        r = n / (n / l);
        w[++m] = n / l;
        g[m] = sum2(w[m]) - 1;
        h[m] = w[m] - 1;
        if (w[m] <= sqr) id1[w[m]] = m;
        else id2[r] = m;
    }
    for (int j = 1; j <= cnt; j++) {
        for (int i = 1; i <= m && pri[j] * pri[j] <= w[i]; i++) {
            int k = getid(w[i] / pri[j]);
            g[i] -= 1ll * pri[j] * (g[k] - suf1[j - 1]);
            h[i] -= h[k] - j + 1;
        }
    }
}

ll phi(ll p, ll k) {
    return quick(p, k - 1) * (p - 1);
}

ll solve(ll x, int y) {
    if (x <= 1 || pri[y] > x) return 0;
    int k = getid(x);
    ll ret = g[k] - suf1[y - 1] - h[k] + y - 1;
    for (int i = y; i <= cnt && 1ll * pri[i] * pri[i] <= x; i++) {
        ll t1 = pri[i], t2 = 1ll * pri[i] * pri[i];
        for (int e = 1; t2 <= x; ++e, t1 = t2, t2 *= pri[i]) {
            ret += 1ll * solve(x / t1, i + 1) * phi(pri[i], e) + phi(pri[i], e + 1);
        }
    }
    return ret;
}
ll mu(ll k) {
    if (k == 1) return -1;
    return 0;
}

ll solve1(ll x, int y) {
    if (x <= 1 || pri[y] > x) return 0;
    int k = getid(x);
    ll ret = -h[k] + y - 1;
    for (int i = y; i <= cnt && 1ll * pri[i] * pri[i] <= x; i++) {
        ll t1 = pri[i], t2 = 1ll * pri[i] * pri[i];
        for (int e = 1; t2 <= x; ++e, t1 = t2, t2 *= pri[i]) {
            ret += 1ll * solve1(x / t1, i + 1) * mu(e) + mu(e + 1);
        }
    }
    return ret;
}

int main() {
    int T = read();
    init(100000);
    while (T--) {
        cin >> n;
        sqr = sqrt(n);
        m = 0;
        calc1();
        cout << solve(n, 1) + 1 << " ";
        cout << solve1(n, 1) + 1 << "\n";
    }
    return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章