对积性函数筛法的理解

积性函数和完全积性函数在欧拉筛里的情况

对于函数\(f(x)\)
然后式子\(f(x y) = f(x) * f(y)\)
如果说只有x和y互质的时候才成立,那么\(f(x)\)是一个积性函数
如果说对于任意的x和y都成立,那么\(f(x)\)是一个完全积性函数

在线性筛里面,如果说\(f(x)\)是一个积性函数,那么

void pri_table(int N){
    vis[1] = vis[0] = 1;
    F[1] = cal(1);
    for(int i = 2; i <= N; i++) {
        if(!vis[i]) {
            pri[++tot] = i;
            F[i] = cal(i);
        }
        for(int j = 1; j <= tot; j++) {
            if(i * pri[j] > N) break;
            vis[i * pri[j]] = 1;
            if(i % pri[j] == 0) {
                  //考虑维护质数时的值更新即可
                break;
            }else F[i * pri[j]] = F[i] * F[pri[j]];//积性函数情况
        }
    }
}

如果是一个完全积性函数,那么可以直接筛

void pri_table(int N){
    vis[1] = vis[0] = 1;
    F[1] = cal(1);
    for(int i = 2; i <= N; i++) {
        if(!vis[i]) {
            pri[++tot] = i;
            F[i] = cal(i);
        }
        for(int j = 1; j <= tot; j++) {
            if(i * pri[j] > N) break;
            vis[i * pri[j]] = 1;
            F[i * pri[j]] = F[i] * F[pri[j]];
            if(i % pri[j] == 0) break;//维护质数时的值更新
        }
    }
}

题目

比如题目传送门
其中\(f(x) = x ^ n\)是一个完全积性函数
\(f(xy) = (xy) ^ n = x^n y^n\)
那么就利用\(F[i * pri[j]] = F[i] * F[pri[j]]\)即可
对于质数就直接利用快速幂求
素数个数\(\frac{1.5n}{logn}\)左右,每个素数的快速幂是\(O(logn)\)那么时间复杂度就是\(O(n)\)

#include <iostream>
#include <cstdio>
#include <cmath>
#define ll long long
using namespace std;
const int N = 1e7 + 5;
const int mod = 1e9 + 7;
int pri[N], tot;
bool vis[N];
ll F[N];
ll pow(ll a, ll b, ll p){
    ll ans = 1; a %= p;
    while(b){
        if(b & 1) ans = ans * a % p;
        a = a * a % p;
        b >>= 1;
    }
    return ans;
}
void pri_table(int N, int n){
    vis[1] = vis[0] = 1;
    F[1] = 1;
    for(int i = 2; i <= N; i++) {
        if(!vis[i]) {
            pri[++tot] = i;
            F[i] = pow(i, n, mod);
        }
        for(int j = 1; j <= tot; j++) {
            if(i * pri[j] > N) break;
            vis[i * pri[j]] = 1;
            F[i * pri[j]] = (ll)F[i] * F[pri[j]] % mod;
            if(i % pri[j] == 0) break;
        }
    }
}
int main(){
    int n;
    scanf("%d", &n);
    pri_table(N - 3, n);
    ll ans = 0;
    for(int i = 1; i <= n; i++) 
        ans = ans ^ F[i];
    printf("%lld\n", ans);

    return 0;
}

对于题目传送门
也是一个线性筛问题,其实\(f(n,kk)\)表示把一个数\(n\)划分为\(kk\)个数字乘积的个数
根据唯一分解定理\(n = p_1^{a_1} p_2^{a_2}...p_k^{a_k} = \prod_{i = 1}^kp_i^{a_i}\)
那么就是说\(f(n,kk) = \prod_{i = 1}^k g(a_i, kk)\)
\(g(a_i, kk)\)表示把\(a_i\)划分为\(kk\)个数字相加的情况数。
因为\(p_i\)都是质数,那么也就是每一个质数都是独立的,因为几个质数的乘积不能用另外几个质数的乘积来表示
比如\(a_1\)的一组方案\(p_1^1 \times p_1^2 \times p_1 ^ 3\)\(a_2\)的一组方案\(p2^2 \times p_2^1 \times p_3^0\)
那么他们组合就是\((p_1^1 p_2^3) \times (p_1^2 p_2^1) \times (p_1^3 p_2^0)\), 总共的方案数就是两个的方案数的乘积

转换为模型\(\sum_{i = 1}^kx_i = t\)的非负整数解的个数

\[g(t, k) = \left(\begin{array}{c} t+k-1 \\ k-1 \end{array}\right) \]

如果是求一个数,那么直接对数字质因子分解,然后求组合数,再乘起来就行了
但是对于n个数字,就需要采用线性筛
把x拆分为\(x = y \times p^q\)
其中\(gcd(y, p) = 1\),且p是素数
那么\(f(x, k) = f(y, k) \times f(p^q, k) = f(y, k) \times \left(\begin{array}{c} q+k-1 \\ k-1 \end{array}\right)\)
其中\(f(p, k) = k\),\(p\)是质数

那么对于质数倍数时的更新从\(C_{num + k - 1} ^ {k - 1}\) 变成了 \(C_{num + k} ^ {k - 1}\)
化简后得到值扩大了\(\frac{num + k}{num + 1}\)倍,其实num表示数字含有的质数个数
那么直接线性筛就行了

#include <iostream>
#include <cmath>
#include <cstdio>
#define ll long long
using namespace std;
const int mod = 1e9 + 7;
const int N = 1e7 + 5;
int pri[N], num[N], tot;
ll F[N], inv[N];
bool vis[N];
void pri_table(int N, ll k){
    F[1] = 1, inv[1] = 1;
    for(int i = 2; i <= N; i++) 
        inv[i] = (ll)(mod - mod / i) * inv[mod % i] % mod;
    for(int i = 2; i <= N; i++) {
        if(!vis[i]) {
            pri[++tot] = i;
            num[i] = 1;
            F[i] = k;
        }
        for(int j = 1; j <= tot; j++) {
            if(i * pri[j] > N) break;
            vis[i * pri[j]] = 1;
            if(i % pri[j] == 0) {
                num[i * pri[j]] = num[i] + 1;
                F[i * pri[j]] = F[i] * (num[i] + k) % mod * inv[num[i] + 1] % mod;
                break;
            }else {
                num[i * pri[j]] = 1;
                F[i * pri[j]] = F[i] * k % mod;
            }
        }
    }
}
int main(){
    int n; ll k; scanf("%d%lld", &n, &k);
    k %= mod;
    pri_table(N - 3, k);
    ll ans = 0;
    for(int i = 1; i <= n; i++)
        ans ^= (F[i] + i) % mod;
    printf("%lld\n", ans);
    return 0;
}

其中逆元的个数可以优化,减少空间,因为逆元最大是\(num[i] + 1\),也就是\(2^n\),n的值也就在32以内

总结

  • 注意函数是积性函数还是完全积性函数,因为两种函数在欧拉筛里放的位置不同
  • 注意当\(i\)\(pri[i]\)的倍数时,所维护值的一个更新情况
posted @ 2020-10-29 15:46  Emcikem  阅读(148)  评论(0编辑  收藏  举报