埃氏筛法+线性筛法+杜教筛+min25筛总结

埃氏筛法

这个筛法是最朴素的筛法了,可以在 O ( n l o g l o g n ) 的时间内(基本 O ( n ) )筛出[1,n]中所有素数。实现非常简单,从2开始遍历,对于每个质数都暴力算出它的所有倍数并筛掉,根据欧拉的调和级数定理,这个时间是 O ( n l o g n ) 级别的,但是只有质数才需要计算倍数,然后不知怎么回事复杂度就变成 O ( n l o g l o g n ) 了。

const int maxn = 100005, N = 100000;
int vis[maxn];
for(int i = 2; i <= N; i++) if(!vis[i])
    for(int j = i + i; j <= N; j += i) vis[j] = 1;

最终vis数组中值为0的即为素数。

应用:给定n,m,求区间[n,m]中的质数个数。 n m 10 12 , m n 10 6
首先我们会发现,如果区间中某个数不是质数,那么它必然有一个小于1E6的质因子。于是我们可以把[1,1E6]范围内的质数全部筛出来,然后用这些质数去筛区间[n,m]中的数字,复杂度应该是 O ( ( m n ) l o g l o g ( m n ) ) 。代码就不放了。

线性筛(欧拉筛法)

顾名思义,它可以在线性时间内筛出区间内质数及积性函数的值。但是在n小于1E6的时候甚至比埃氏筛法慢(因为常数大),于是它的主要功能就变成了求积性函数的值。
考虑如何做到线性。如果我们把每个数字都用它的最小质因子筛去的话就行了,但是怎么做呢?
从小到大枚举每个数字,再枚举当前已经筛出来的质数,筛掉数字乘质数的值。如果数字是当前质数的倍数就可以退出了,因为质数继续枚举就不是最小质因子了。因此此时效率为 O ( n ) 。代码如下:

const int maxn = 100005, N = 100000;
int vis[maxn], prime[maxn], cnt;
for(int i = 2; i <= N; i++){
    if(!vis[i]) prime[++cnt] = i;
    for(int j = 1; j <= cnt; j++){
        int p = prime[j], mul = p * i;
        if(mul > N) break;
        vis[mul] = 1;
        if(i % p == 0) break;
    }
}

上面说过了,这个东西还可以筛出积性函数的值。积性函数定义为对于任意互质的数对 i , j f ( i ) f ( j ) = f ( i j ) 。我们上面实际上算出了每个数的最小质因子,当然也可以算出积性函数的值了,当然我们需要计算任意的质数以及单个质数的幂次对应的f的值,因为这样才可以利用质因数分解计算出任意数字的f。

const int maxn = 100005, N = 100000;
int f[maxn], pw[maxn], vis[maxn], prime[maxn], cnt;
//pw[i]为i最小质因子的次数
for(int i = 2; i <= N; i++){
    if(!vis[i]){
        prime[++cnt] = i;
        pw[i] = 1;
        f[i] = ...;//要求给出i为质数时f(i)的值
    }
    for(int j = 1; j <= cnt; j++){
        int p = prime[j], mul = p * i;
        if(mul > N) break;
        vis[mul] = 1;
        if(i % p == 0){
            f[mul] = f[i / pow(p, pw[i])] * f[pow(p, pw[i] + 1)];//要求给出i为质数的整数次幂时f的值
            pw[mul] = pw[i] + 1;
            break;
        } else pw[mul] = pw[i] * pw[p];
    }
}

当然了,具体计算的时候可以用一些方法把pow去掉。接下来就来列举一些积性函数的求值:

欧拉函数

φ ( n ) = 小于等于n且与n互质的数的个数。定理:

φ ( n ) = n p i 1 p i , n = p i e i , p i

因此我们会发现,若p为n的一个质因子,则有 φ ( n p ) = p · φ ( n ) .当p为质数时 p h i ( p ) = p 1 。于是就可以愉快地写筛法了:

const int maxn = 100005, N = 100000;
int vis[maxn], phi[maxn], prime[maxn], cnt;
phi[1] = 1;
for(int i = 2; i <= N; i++){
    if(!vis[i]){
        prime[++cnt] = i;
        phi[i] = i - 1;
    }
    for(int j = 1; j <= cnt; j++){
        int p = prime[j], mul = p * i;
        if(mul > N) break;
        vis[mul] = 1;
        if(i % p == 0){
            phi[mul] = phi[i] * p;
            break;
        } else phi[mul] = phi[i] * phi[p];
    }
}

于是我们就可以在 O ( n ) 的时间内处理出[1,n]中所有数字的欧拉函数值了。

莫比乌斯函数

μ ( n ) = { 1 n = 1 ( 1 ) k n = p 1 p 2 p k , p i 0 o t h e r s

当n含有平方因子时函数值为0,n为质数时函数值为1,于是也可以线性筛了。

const int maxn = 100005, N = 100000;
int vis[maxn], mu[maxn], prime[maxn], cnt;
mu[1] = 1;
for(int i = 2; i <= N; i++){
    if(!vis[i]){
        prime[++cnt] = i;
        mu[i] = -1;
    }
    for(int j = 1; j <= cnt; j++){
        int p = prime[j], mul = p * i;
        if(mul > N) break;
        vis[mul] = 1;
        if(i % p == 0) break;//mul含有平方因子,mu值为0
        mu[mul] = -mu[i];//实际上是mu[mul] = mu[i] * mu[p];
    }
}

于是我们的线性筛就可以用来做题了!

例题

i = 1 n j = 1 n g c d ( i , j )

求出上式的值,其中 n 10 6
根据欧拉函数的性质 d | n φ ( d ) = n ,可以化简上式得到
i = 1 n j = 1 n g c d ( i , j ) = i = 1 n j = 1 n d | i , d | j φ ( d ) = d = 1 n φ ( d ) n i 2

于是我们只需要计算出区间中欧拉函数的值就行了,复杂度 O ( n ) 。当然这道题可以利用下面说的杜教筛进一步优化,把复杂度降到 O ( n 2 3 )

杜教筛

杜教筛是求积性函数前缀和的一种方法。考虑如下的题目:
请求出如下表达式的值( n 10 9 ):

ϕ ( n ) = i = 1 n φ ( i )

考虑使用欧拉函数的性质 d | n φ ( d ) = n ,得到 φ ( n ) = n d | n , d < n φ ( d ) 。带入得到:
ϕ ( n ) = i = 1 n i d | i , d < i φ ( d )

考虑枚举 i d 的值进行计算:
ϕ ( n ) = n ( n + 1 ) 2 i = 2 n d = 1 n i φ ( d ) = n ( n + 1 ) 2 i = 2 n ϕ ( n i )

我们会发现后面那个向下取整的式子中只有 O ( n ) 种不同的值,因此可以整除分块,带上记忆化搜索复杂度是 O ( n 3 4 ) 。但是我们可以用线性筛预处理出前 O ( n 2 3 ) 个欧拉函数的前缀和,杜教筛就可以取到最好的复杂度,为 O ( n 2 3 )
同时我们会发现,杜教筛不仅求出了 ϕ ( n ) ,也求出了任意的 p h i ( n i ) 。这个东西对于做题是非常有帮助的,比如上面最大公约数之和那题,我们就可以在外面的求和也整除分块一下,利用上面的值在较好的复杂度内完成算法。

例题:互质数对

求出所有的有序数对 ( i , j ) 使 i , j i n , j n , n 10 9 。即如下算式:

i = 1 n j = 1 n [ gcd ( i , j ) = 1 ]

考虑莫比乌斯函数的性质 d | n μ ( d ) = [ n = 1 ] ,原式转化为
i = 1 n j = 1 n d | i , d | j μ ( d ) = d = 1 n μ ( d ) n i 2

于是我们的目标是求出莫比乌斯函数的前缀和。仿照上面的做法:
M ( n ) = i = 1 n μ ( i ) = i = 1 n [ i = 1 ] d | i , d < i μ ( d ) = 1 i = 2 n M ( n i )

于是我们就顺利地在 O ( n 2 3 ) 的时间内求出了莫比乌斯函数前缀和,上面那题也解决了。
其实我们也可以直接利用欧拉函数的定义计算这道题,答案其实就是 2 φ ( n ) 1

例题:最小公倍数之和

求出如下表达式的值( n 10 9 ):

i = 1 n j = 1 n l c m ( i , j ) = i = 1 n j = 1 n i j gcd ( i , j )

枚举最大公因数,可以得到( 下面默认除法为向下取整):
d = 1 n d i = 1 n / d j = 1 n / d i j · [ gcd ( i , j ) = 1 ]

f ( i ) = j = 1 i j · [ gcd ( i , j ) = 1 ] = j = 1 i j · d | i , d | j μ ( d ) = d | i μ ( d ) · d · i d ( i d + 1 ) 2 = i 2 ( d | i μ ( d ) i d + μ ( d ) ) = i 2 ( φ ( i ) + [ i = 1 ] )

于是题目就可以化简为:
d = 1 n d ( 2 i = 1 n / d i · f ( i ) 1 ) = d = 1 n d i = 1 n / d i 2 φ ( i ) = d = 1 n d · g ( n d )

于是我们需要计算出g函数的前缀和即可。我们会发现如下性质: g ( n ) = n 3 n 2 d | n , d < n φ ( d ) ,于是杜教筛:
G ( n ) = i = 1 n g ( i ) = i = 1 n i 3 i 2 d | i , d < i φ ( d ) = i = 1 n i 3 i = 2 n d = 1 n / i ( i d ) 2 φ ( d ) = n 2 ( n + 1 ) 2 4 i = 2 n G ( n i ) · i 2

于是这道题就可以在 O ( n 2 3 ) 的时间内解决了。

Min_25筛

在大部分情况下,杜教筛已经可以解决很多问题了,但是仍然有很多毒瘤题解决不了。这个时候就可以试一试Min_25筛(我也不造为啥叫这个名字),复杂度为 O ( n 3 4 log n ) (我也不造为啥是这个复杂度)。
这个筛法常数比较小,内存也比较小,应该算一种挺不错的筛法了。我们以筛出[1,n]中的质数个数为例来说明这个筛法。
首先,他假设了[2,n]中所有数字全是质数,然后用数论办法模拟进行埃氏筛法。其实埃氏筛法中质数大小到 O ( n ) 的级别时就没有数字可以被筛掉了。先预处理出这些质数,就可以开始Min_25筛了。我们令状态g(n,j)表示当前筛的区间是[1,n],已经筛完了前j个质数时剩下来的数字个数(即所有最小质因子大于第j个质数的数字和所有质数的总个数)。
我们考虑用g(n,j-1)推出g(n,j)。j-1时比j多了一些最小质因子为第j个质数的数字,要把他们减掉,由于g(n,j-1)中还存在比第j个质数小的质数,我们要把这些特殊情况去掉:

g ( n , j ) = g ( n , j 1 ) g ( n p j , j 1 ) + j 1

可以证明,这样的复杂度是 O ( n 3 4 log n ) 的。

//id1和id2是离散化的数组,tot是小于sqrt(n)的质数个数
//prime是质数数组,num数组记n/i向下取整不同的值,从大到小排序,cnt为这些值的个数。
int sqr = (int)(sqrt(n) + 1);
for(int i = 1; i <= tot; i++){
    int p = prime[i];
    for(int j = 1; j <= cnt && p * p <= num[j]; j++){
        int d = num[j] / p, k = d <= sqr ? id1[d] : id2[n / d];
        g[j] -= g[k] - j + 1;
    }
}

初始值就是把[2,n]中所有数字都当成质数的结果。当然,除了质数个数,我们还可以筛出来很多其它的东西。
f ( x ) 在把所有数字都当成质数时是完全积性函数,即任意的 x , y f ( x ) f ( y ) = f ( x y ) ,我们可以用g数组筛出这些东西。只需要稍微修改一下上面的递推式即可:

g ( n , j ) = g ( n , j 1 ) f ( p j ) ( g ( n p j , j 1 ) i = 1 j 1 f ( p i ) )

考虑如何利用这个东西求出 积性函数 g ( x ) 的前缀和。首先我们可以通过拆分等方法把所有质数的函数值之和算出来,然后定义S(n,j)为区间[2,n]中所有最小质因子大于等于第j个质数的数字函数之和,我们可以暴力枚举那些数字的最小质因子去计算,即得到
S ( n , j ) = g ( n , | P | ) i = 1 j 1 g ( p i ) + k j , p k 2 n   e 1 , p k e + 1 n S ( n p k e , k + 1 ) · g ( p k e ) + g ( p k e + 1 )

我也不造咋证的,这部分暴力计算的复杂度也是 O ( n 3 4 log n ) ,于是我们就愉快的做完了。

例题

求如下表达式的值( m 10 9 ,   n 10 12 ):

i = 1 n μ ( i n )

首先若n含有平方因子则答案为0,否则我们考虑i的质因子中是否跟n有相同的。首先我们用g筛出质数个数,然后来搞一搞S的递推式(其实如果枚举质数的次数超过1就不用算了,莫比乌斯函数值必然为0):
S ( n , j ) = g ( n , | P | ) j + 1 i = j c n t [ p i | n ] i = j c n t S ( n p i , i + 1 )

最后再加上1的情况,然后这道题就愉快地在 O ( m 3 4 log m + n ) 的时间内做完了……

推荐Min_25筛题目:51nod奇怪的数学题,loj简单的函数

猜你喜欢

转载自blog.csdn.net/WAautomaton/article/details/82667522
今日推荐