积性函数筛法(Insider Preview)

以下内容仅供指教,请勿当真。

如有侵权,请与我联系(我相信是没有的)。

Should be deleted on July 21st.


开局一个 Markdown,内容全靠编。——Orange

积性函数筛法

杜教筛

快速复习

(不再赘述,自行复习)

1. 数论分块
2. 积性函数

杜教筛

1. 杜教筛是干什么的

设积性函数 f ( i ) ,我们的问题是求:

i = 1 n f ( i )

换句话说,我们的问题是求 积性函数的前缀和。利用杜教筛可以在优于线性时间的复杂度内解决这个问题。但并不是所有积性函数的前缀和都能使用杜教筛来求,必须满足一定条件(参见 适用范围)。

2. 一般形式

设积性函数 f ( i ) ,定义它的前缀和为:

S ( n ) = i = 1 n f ( i )

如果有下面这个式子:
i = 1 n ( f g ) ( i )

f 卷上 g 的前缀和,那么我们有:
i = 1 n d i f ( i ) g ( i d ) = i = 1 n g ( i ) j = 1 n i d ( j )

该等式表示枚举 g 的参数 i ,再枚举 i 的倍数 j ,考虑 ( f g ) ( i j ) 对答案的贡献。注意到:
i = 1 n d i f ( i ) g ( i d ) = i = 1 n g ( i ) j = 1 n i d ( j ) = i = 1 n g ( i ) S ( n i )

即我们得到了:
i = 1 n g ( i ) S ( n i ) = i = 1 n ( f g ) ( i )

将左式拆开移项到右式,可得:

g ( 1 ) S ( n ) = i = 1 n ( f g ) ( i ) i = 2 n g ( i ) S ( n i )

如果卷积的前缀和以及 g 都易于计算,那么我们能够在更优的时间内解决这个问题。具体的分析我们将在下面以例题的形式呈现。

3. e.g. 51NOD 1244 莫比乌斯函数之和

给定一组询问 ( a , b ) ,试求:

i = a b μ ( i )


①杜教筛

首先将问题转化为前缀和相减的形式,然后考虑对原式进行变化。

根据莫比乌斯等式,我们有:

d n μ ( d ) = [ n = 1 ]

即:
μ 1 = ϵ

我们取 g = 1 1 是常函数),代入一般形式可得:
S ( n ) = i = 1 n ϵ ( i ) i = 2 n S ( n i ) = 1 i = 2 n S ( n i )

对右侧进行数论分块,递归进行计算即可。

②时间复杂度

设用于计算前缀和的表达式为:

S ( n ) = H ( n ) i = 2 n S ( n i )

考虑到实际情况,我们不妨假设计算 H ( n ) 的时间复杂度为 O ( 1 )

在求解 S ( n i ) 时,我们会做相同的工作,去求解 S ( n i j ) ;类似地,在更深层的递归我们也会求结构类似的式子。注意到该式等于 S ( n i j ) ,类似地在更深层递归会求解 S ( n i j k ) 。由此我们可以得出:根据数论分块,调用 S ( x ) 时参数 x 的取值总共就只有 O ( n ) 种,不会因为递归调用而变多。但是注意,如果我们不记忆化的话,我们是有可能重复计算的。因此我们需要用哈希表进行记忆化,这里假设哈希表的时间复杂度为 O ( 1 )

对于 S ( n ) ,我们要花上 O ( n ) 的时间复杂度去枚举转移,因此对于所有的调用,时间复杂度为:

i = 1 n ( i + n i )

上式中的 i n i 表示数论分块的 O ( n ) 种取值, i n i 表示计算它们需要的时间。我们用积分粗糙地代替前缀和:
0 n i   d i + 0 n n i   d i

2 3 n 3 2 + 2 n ( n ) 1 2

计算得时间复杂度为 O ( n 3 4 )


O ( n 3 4 ) 还是太大,怎么办?当 n 较小的时候,我们是可以用线性筛预处理的。所以我们需要预处理到一个合适的值,使得总时间复杂度最小。

假设我们预处理到阈值 k 。根据前面的分析,我们在杜教筛中的数论分块只需要计算大于 k 的部分:

O ( k + i = 1 n k n i )

根号表示数论分块需要的时间复杂度。

把前缀和用积分粗糙处理,那么上式等于:

O ( k + 2 n ( n k ) 1 2 ) = O ( k + n k )

k = n k ,即 k = n 2 3 ,时间复杂度最优,为 O ( n 2 3 )

③实现

实现时,必须使用 Hash 表记忆化注意数论分块的循环变量要开 l o n g   l o n g 实际操作时,必须使用线性筛筛出前面较小的部分,Hash 表也要尽量开大点。

LL sieve(LL n)
{
    if (n <= threshold) return mu[n];
    LL ans;
    if ((ans = hash.query(n)) != -1) return ans;
    ans = 1;
    for (LL i = 2, t; i <= n; i = t + 1)
    {
        t = n / (n / i);
        ans -= (t - i + 1) * sieve(n / i);
    }
    hash.insert(n, ans);
    return ans;
}
4. e.g. 51NOD 1244 欧拉函数之和

给定一组询问 n ,试求:

i = 1 n φ ( i )


我们知道:

d n φ ( d ) = n

即:
φ 1 = i d

代入杜教筛:
S ( n ) = i = 1 n i i = 2 n 1 S ( n i )

即:
S ( n ) = n ( n + 1 ) 2 i = 2 n S ( n i )

用上面的方法即可。

LL sieve(LL n)
{
    if (n <= threshold) return phi[n];
    if (map.count(n)) return map[n];
    LL ans = (n & 1) ? (n % mod * (((n + 1) >> 1) % mod)) : ((n >> 1) % mod * ((n + 1) % mod));
    for (LL i = 2, t; i <= n; i = t + 1)
    {
        t = n / (n / i);
        ans = (ans - (t - i + 1) % mod * sieve(n / i)) % mod + mod;
    }
    return map[n] = ans % mod;
}
5. 适用范围

对于积性函数 f ,如果能找到另一个积性函数 g ,并且 g f g 的前缀和能在 O ( 1 ) 的时间复杂度内求出,那么就可以用杜教筛求 f 的前缀和。

Min_25 筛

由 Min_25(山之内宏彰)提出,故称 Min_25 筛。

好像网上很多写的 Min_25 筛在原理部分都是假的。

1. Min_25 筛是干什么的

也是拿来求积性函数前缀和的,但适用范围比杜教筛广多了(参见适用范围)。除此之外,它的步骤中还能求出质数的函数值之和。

2. 一般形式

设积性函数 f ( i ) ,定义它的前缀和为:

S ( n ) = i = 1 n f ( i )

我们要求的是 S ( n )

注意,由于 1 既不是质数也不是合数,并且根据积性函数的定义, f ( 1 ) 始终等于 1 ,因此我们计算的目标是:

S ( n ) = 1 + i = 2 n f ( i )

一个重要定义

已知 f ( i ) 是积性函数:

struct Interger
{
    unsigned long long x;
    bool isPrime;
};
Interger f(Interger x)
{
    if (x.isPrime)
        return something;
    else
    {
        // do something
    }
}

我们设 f ( i ) 为:

Interger f2(Interger x)
{
    x.isPrime = true;
    return f(x);
}

即:无论 i 是不是质数,在 f ( i ) 中我们都把 i 当作一个质数代入 f ( i ) 中计算。以欧拉函数为例,可以把 φ 定义如下:

Interger phi(Interger x)
{
    if (x.isPrime)
        return x - 1;
    else
    {
        // do something
    }
}
Interger phi2(Interger x)
{
    return x - 1;
}
步骤 1:求质数的函数值之和

我们要求:

i = 2 n [ i  是质数 ] f ( i )

我们不妨求:
i = 2 n [ i  是质数 ] f ( i )

根据定义,两式显然相等。


p i 表示第 i 个质数,设:

g ( n , j ) = i = 2 n [ i  是质数,或者  i  的最小质因子   p j ] f ( x )

p k 是最大的满足 p k 2 n 的质数,那么显然,我们要求的东西等于 g ( n , k ) 。因为 p k + 1 2 是以 p k + 1 为最小质因子的最小合数,而此时 p k + 1 > n ,所以不存在一个最小质因子大于 p k 的合数对答案有贡献;换句话说, g ( n , k ) 中只有质数对答案有贡献。

同理,我们可以得到:

g ( n , j ) = g ( n , j 1 ) ( p j 2 > n )


如果 p j 2 n ,那么最小质因子是 p j 的合数对 g ( n , j 1 ) 有贡献。到 g ( n , j ) 那里,就相当于要不考虑它们的贡献,所以有:

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

省略号就是最小质因子是 p j 的合数对 g ( n , j 1 ) 的贡献,考虑求它们。首先我们很自然地会想到减去最小因数包含 p j 的数的函数值。为了计算它,我们 要求 f 完全积性函数。这样就可以把 p j 移至函数外,在外面直接乘上 f ( p j )
f ( p j ) g ( n p j , j 1 )

我们不妨假设一个数等于两个质数相乘: n = p j p l 。显然我们要求 p j 要小于等于 p l ,但是仔细观察上式,发现 n p j 这部分不仅一定有大于等于 p j 的数(因为 p j 2 < n ),而且一定包含比 p j 小的数,也就是说我们减多了,需要把比 p j 小的加回来:
f ( p j ) g ( p j 1 , j 1 )

这样我们就得到了 g 的完整表达式:
g ( n , j ) = { g ( n , j 1 ) p j 2 > n g ( n , j 1 ) f ( p j ) ( g ( n p j , j 1 ) g ( p j 1 , j 1 ) ) p j 2 < n


再考虑下 g ( p j 1 , j 1 ) 表示的是什么。因为 p j 1 < p j 2 ,显然它不可能包含合数对答案的贡献,所以 g ( p j 1 , j 1 ) 实际上等于:

i = 1 j 1 f ( p i )

所以 g 的最终表达式是:
g ( n , j ) = { g ( n , j 1 ) p j 2 > n g ( n , j 1 ) f ( p j ) ( g ( n p j , j 1 ) i = 1 j 1 f ( p i ) ) p j 2 < n


g 虽然不是前缀和,但是它是质数的函数值之和。除了作为 Min_25 筛的步骤,它还有特殊用途,比如可以拿它来求质数个数:令 f = i d 即可。

步骤 2:求所有数的函数值之和

我们要求:

S ( n )


设:

s ( n , j ) = i = 2 n [ i  是质数,或者  i  的最小质因子   p j ] f ( x )

根据定义,显然有:
s ( n , k + 1 ) = g ( n , k )

注意这里是大于等于; k 的定义在 前面。显然 S ( n ) = s ( n , 1 )

我们考虑用 s ( n , j + 1 ) 来计算 s ( n , j ) ,显然我们需要加上最小质因子为 p j 的合数对答案的贡献。

s ( n , j ) = s ( n , j + 1 ) +

现在我们只保证了 f ( i ) 是积性函数,因此必须枚举 p j 的指数。
e = 1 , p j e p n ( f ( p j e ) ( s ( n p j e , j + 1 ) s ( p j , j + 1 ) ) + f ( p j e + 1 ) )

中间是这样的原因同 g s ( p j , j + 1 ) 也等于 i = 1 j + 1 f ( p i ) 。最后单独加上 f ( p j e + 1 ) 的原因也很好理解:之前没有算上,左边的也没有算上,为什么不加呢?

为什么这里可以直接相乘呢?注意到,这里是 j + 1 而不是 j 1 ,所以这里面的数的最小质因子都是 p j + 1 ,当然可以直接相乘了。

为什么这里是 s ( p j , j + 1 ) ,前面是 g ( p j 1 , j 1 ) ?因为前面我们不打算算上 p j 对答案的贡献,我们已经算过了;而这里我们也不打算算上 p j 对答案的贡献,也是因为我们已经算过了,但这里是加,所以在删去额外部分时就要把 p j 一并删去。

为什么要求 p j e + 1 n 而不是 p j e n ?因为我们实际是要要求 n p j e p j

想办法把上面的推导变成代码即可。

3. 实践
①求 i d g ( n , k )

我们实际上是要探究如何求 g ( n , k )

首先,我们需要用线性筛求出 n 以内的所有质数。注意到,第一维的变化方法只有:

n p i p j p l

也就是说 第一维的取值是 O ( n ) ,并且 i n i 必有其一小于等于 n 。我们用数论分块求出所有可能的取值,用两个大小为 O ( n ) 的数组保存编号即可。

注意到,第二维的变化是从 j 1 j 的,因此我们可以把第二维看作一层,使用滚动数组依次计算即可。

int N;
LL appear[maxn * 2 + 5];
int id[2][maxn + 5];
LL g[maxn * 2 + 5];
void initBlocks()
{
    N = 0;
    for (LL i = 1, t; n / i > 1; i = t + 1) // 不需要用到 g(1)
    {
        t = n / (n / i);
        appear[++N] = n / i;
        if (appear[N] <= sqrtN) id[0][appear[N]] = N;
        else id[1][n / appear[N]] = N;
    }
}
inline LL& get(LL x)
{
    if (x <= sqrtN) return g[id[0][x]];
    else return g[id[1][n / x]];
}
void solveG()
{
    for (int i = 1; i <= N; i++)
        g[i] = appear[i] - 1;

    for (int j = 1; j <= k; j++)
        for (int i = 1; i <= N && (LL)prime[j] * prime[j] <= appear[i]; i++) // 从大到小计算
            g[i] = g[i] - (get(appear[i] / prime[j]) - (j - 1));
}

注意最后的计算顺序,用从大到小的顺序计算可以保证被覆盖掉的值在本次计算中不会被用到,这样就不用滚动数组了。最后的答案是 get(n)

②求 g 的时间复杂度

显然,这一步的时间复杂度绝不会超过 O ( n log n ) 。但实际上,由于中间要求 p j 2 n ,因此这个还需要分析。可以认为时间复杂度是 O ( n 3 4 log n ) 的。实际表现是,当 n 10 11 时,更新 g 的代码的执行次数不足 10 8 次;当 n 10 12 时,约执行了 4 × 10 8 次;当 n 10 10 时,约执行了 2 × 10 7 次;而当 n 10 9 时, 约执行了 3 × 10 6 次。

③求 φ g ( n , k )

已知:

φ ( n ) = n 1

它不是积性函数,怎么解决呢?

由于我们求的是和,因此可以把它拆成 n 1 分别求值。一般地,如果 f ( x ) 能够写成一个多项式,那它就能用 Min_25 筛解决。

具体内容见下面部分。

④求 φ s ( n , 1 )

重点来了, s ( n , 1 ) 才是我们想要的东西。如何求 s ( n , 1 ) 呢?

首先 s ( n , k + 1 ) = g ( n , k ) 。注意到,我们把 f 拆成两部分的理由是要保证我们求的东西是完全积性函数,而这里我们只用保证 s 是积性函数就可以了,因此我们把所有的 g 合并起来即可。

如果你直接像我第③点这么做,你就会 WA 得很惨。注意,既然我们把 g 拆成两部分求的理由是保证 f 0 1 都是完全积性函数,那么你验证一下, f 0 ( x ) = 1 是完全积性函数吗?

f 0 ( a ) f 0 ( b ) = ( 1 ) ( 1 ) = 1 f 0 ( a b )

根据 f 0 ( x ) 的定义, f 0 ( a b ) = 1

所以拆开后要检查一下 f i 是否为积性函数。一般来说, f 是一个多项式。拆开时,如果某项系数不为 1 ,那么就需要把该项系数拿开,代入完全积性函数 i d e 计算,算完后再乘以系数。

⑤求 s 的时间复杂度

可以认为时间复杂度仍然是 O ( n 3 4 log n ) ,证明超出了我们的讨论范围。

4. 适用范围

对于积性函数 f ,如果对于一个质数 p f ( p ) 的表达式是一个完全积性函数或者多个完全积性函数乘以常系数的和(比如一个多项式),并且 f ( x k ) 能够在 O ( 1 ) 的时间复杂度内求出(或者在 O ( k ) 的时间复杂度内递推求出,或者在 O ( n ) 的时间复杂度内预处理……),那么就可以用 Min_25 筛求 f 的前缀和。(如果你是跳跃着阅读到这的,你可以认为:Min_25 筛能够求绝大部分积性函数的前缀和。)

5. 过程总结

观察一下,Min_25 筛实际上是先构造了一个函数 g ,使得:

i = 2 n [ i  是质数 ] g ( i ) = i = 2 n [ i  是质数 ] f ( i )

主要过程是:从所有 g 的和推算到质数的 g 的和,由此得到质数的 s 的和,再推算到所有 s 的和。

练习部分

下面的练习可能可以使用别的筛法通过,但是这里都用 Min_25 筛实现。

e.g. 51NOD 1244 莫比乌斯函数之和
解决方法

注意到 μ ( x ) = 1 并不是完全积性函数,但是 μ ( x ) 是。我们求 g 的时候筛 μ ( x ) 就可以了。实际上求的就是质数个数。

在求 s 时,不必枚举次数,因为当 e 2 时, μ ( p j e ) = 0 ,对答案没有贡献。

参考代码
// 见 "Source Code\数论"
实际效率

这道题由于没有取模操作,也不用枚举指数,因此常数很小,甚至可以和使用哈希表的杜教筛媲美。

注意事项

在计算 g ( p j 1 , j 1 ) s ( p j , j + 1 ) 时,不能够尝试直接从数组中获取,因为它们可能没有标号。应该直接计算它们。如果不能 O ( 1 ) 得到所有质数的函数值之和,就应该在线性筛中预处理,时间复杂度 O ( n )

e.g. JZOJ 5760 湖人
题目大意

求:

i = 1 n 1 σ 0 ( i )

运算在模 p 意义下进行, p 为给定质数。 n 10 9

(原题还需要转换,但不在这里的讨论范围内)

解决方法

f ( i ) = 1 σ 0 ( i ) ,显然 f ( i ) = 1 2

g 时,需要把 f 拆成 1 2 × 1 ,相当于还是求质数个数。

s 时,需要用到 1 e ,预处理即可,似乎只用预处理 O ( log n ) 个。

参考代码
void solveG()
{
    for (int i = 1; i <= N; i++)
        g[i] = appear[i] - 1;
    for (int j = 1, p = prime[j]; j <= k; j++, p = prime[j])
        for (int i = 1; i <= N && p * p <= appear[i]; i++)
            g[i] = ((LL)g[i] - (g[getId(appear[i] / p)] - (j - 1)) + mod) % mod;
}
void solveS()
{
    for (int i = 1; i <= N; i++)
        s[i] = (LL)g[i] * inv[2] % mod;
    for (int j = k, p = prime[j]; j; j--, p = prime[j])
        for (int i = 1; i <= N && p * p <= appear[i]; i++)
        {
            int power = p;
            for (int e = 1; p <= appear[i] / power; e++, power *= p)
                s[i] = (s[i] + (LL)inv[e + 1] * (s[getId(appear[i] / power)] - ((LL)inv[2] * j % mod)) % mod + inv[e + 2] + mod) % mod;
        }
}
e.g. LOJ 6053 简单的函数
题目大意

函数 f ( x ) 满足:

f ( 1 ) = 1

f ( p c ) = p c ( p  是质数 )

f ( a b ) = f ( a ) f ( b ) ( gcd ( a , b ) = 1 )

求:

( i = 1 n f ( i ) ) mod ( 10 9 + 7 )

n 10 10

解决方法

显然 f ( x ) 是个积性函数,用 Min_25 筛。

现在 f ( x ) = x 1 ,显然不是完全积性函数。不过注意到,我们需要的是 f ( p ) ( p  是质数 ) ,我们只要构造一个 f 使得 f ( p ) 能用 f ( p ) 算出即可。我们令:

f ( x ) = x 1

x 为奇数时,显然 x 1 = x 1 。在所有的质数中,只有 2 是偶数, ( 2 1 ) ( 2 1 ) = 2 ,我们令:
s ( n , k + 1 ) = g ( n , k ) + 2

即可得到正确的 s ( n , k + 1 )

最后的问题就是如何计算 s ( p j , j + 1 ) 。还是同样的思路,用前 j 个质数的和减去 j 再加上 2 就好了。

参考代码
// 见 "Source Code\数论"
e.g. SPOJ DIVCNT3 Counting Divisors
题目大意

求:

S 3 ( n ) = i = 1 n σ 0 ( i 3 )

n 10 11 ,对于极限数据要求在 10 秒内出解。

解决方法

考虑 σ 0 ( n ) 是如何计算的:

σ 0 ( p 1 r 1 p 2 r 2 p k r k ) = ( r 1 + 1 ) ( r 2 + 1 ) ( r k + 1 )

那么显然, σ 0 ( n 3 ) 等于:
( 3 r 1 + 1 ) ( 3 r 2 + 1 ) ( 3 r k + 1 )

同时显然, f ( n ) = σ 0 ( n 3 ) 也是一个积性函数,考虑用 Min_25 筛。

如果你已经熟练掌握了前面的几道题,这道题应该算裸题吧……唯一需要注意的地方是,当 n = 1 时,将不会进行数论分块的过程,因此直接写 s[1] 会导致读取到上一次的数据。最好的解决方法是在计算前令 s[0] = 0

参考代码
// 见 "Source Code\数论"

猜你喜欢

转载自blog.csdn.net/lycheng1215/article/details/80954668