【知识总结】线性筛_杜教筛_Min25筛
首先感谢又强又嘴又可爱脸还筋道的国家集训队(Upd: WC2019 进候选队,CTS2019 不幸 rk6 退队)神仙瓜 ( jumpmelon ) 给我讲解这三种筛法~~
由于博主的鸽子属性,这篇博客可能会无限期咕咕咕
线性筛
这种算法是比较基础的筛法,在入门时就已经学习用它来筛一定范围内的质数了,因此具体算法流程无需赘述。但在筛质数的基础上,这种算法由于其优越性质在处理数论函数时也被广泛应用。这里直接给出筛出小于 \(N\) 的质数的模板。
void init()
{
for (int i = 2; i < N; i++)
{
if (!mark[i])
prime[cnt++] = i;
for (int j = 0; j < cnt && (ll)i * prime[j] < N; j++)
{
int k = i * prime[j];
mark[k] = true;
if (i % prime[j] == 0)
break;
}
}
}
知其然也要知其所以然,不能像我一样一年线性筛都是背模板,前几天才仔细想过原理qwq。筛去每个合数的一定是它的最小质因子。由于最小质因子是唯一的,所以每个合数只会被筛去一次,这就保证了线性的复杂度。考虑对于有超过一个质因子的合数 \(T\) ,它的最小质因子是 \(p\) ,任一非最小质因子是 \(p'\) ,那么 \(\frac{T}{p'}\) 一定含有质因子 \(p\) 。因此当上述代码中 \(i=\frac{T}{p'}\) 时,\(prime[j]=p\) 时就会退出循环,无法循环到 \(prime[j]=p'\) 的情况。因此上述结论成立。
上面提到过,线性筛经常被用于处理一些数论函数,尤其常见于积性函数(非积性函数能不能搞我不知道,反正我没见过)。下面的代码处理了莫比乌斯函数 \(\mu(x)\) 和欧拉函数 \(\varphi(x)\) 。这段代码是我现写的,不知道有没有锅……
void init()
{
mu[1] = phi[1] = 1;
for (int i = 2; i < N; i++)
{
if (!mark[i])
prime[cnt++] = i, mu[i] = -1, phi[i] = i - 1;
for (int j = 0; j < cnt && (ll)i * prime[j] < N; j++)
{
int k = i * prime[j];
mark[k] = true;
if (i % prime[j] == 0)
{
mu[k] = 0;
phi[k] = phi[i] * prime[j];
break;
}
else
{
mu[k] = -mu[i];
phi[k] = phi[i] * phi[prime[i]];
}
}
}
}
首先,由于 \(\mu\) 和 \(\varphi\) 都是积性函数,所以当 \(i\) 和 \(prime[j]\) 互质时直接把它们的函数值乘起来即可。
先看 \(\mu\) 的部分。根据定义,当 \(x\) 是质数时 \(\mu(x)=-1\) ;当 \(x\) 是某质数平方的倍数时,\(\mu(x)=0\) 。由于当 \(i\equiv 0 \mod prime[j]\) 时 \(k\) 是 \(prime[j]^2\) 的倍数,所以 \(mu[k]=0\) 。
再看 \(\varphi\) 的部分。当 \(i\equiv 0 \mod prime[j]\) 时,考虑欧拉函数的公式 \(\varphi(x)=x\prod (1-\frac{1}{p_i})\) (其中 \(p_i\) 取遍 \(x\) 的所有质因子) ,发现求积符号里面没有变化(没有新增质因子),只是 \(x\) 变成了 \(x\cdot p\) ,所以 \(phi[k]=phi[i]*prime[j]\) 。
这两个常见的例子启示我们:想用线性筛处理积性函数 \(f(x)\) ,就要解决 \(x\) 是质数的情况和已知 \(f(i)\) 求 \(f(i\cdot p)\) ,其中 \(p\) 是 \(i\) 的质因子的情况(\(i\) 与 \(p\) 互质的情况直接积性函数性质 \(f(i\cdot p)=f(i)\cdot f(p)\) 就好了)。这样的用例如 【洛谷2257/BZOJ2820】YY的GCD(数论/莫比乌斯函数) (我远古时期的博客……)。
然鹅事实上很多情况下上述后一种情况的方案并不是很好构造……于是这里有一个比较通用的方法:设 \(last[i]\) 表示 \(i\) 的最小质因子 \(p\) 的一个幂 \(p^k\) ,使 \(p^k\) 能整除 \(i\) 且 \(k\) 最大(这个表述不太友好,但是我想不出来友好的表述qwq)。那么处理 \(last[i]\) 的方法显而易见:
void init()
{
for (int i = 2; i < N; i++)
{
if (!mark[i])
prime[cnt++] = last[i] = i;
for (int j = 0; j < cnt && (ll)i * prime[j] < N; j++)
{
int k = i * prime[j];
mark[k] = true;
if (i % prime[j] == 0)
{
last[k] = last[i] * prime[j];
break;
}
else
last[k] = prime[j];
}
}
}
此时,当\(i\equiv 0 \mod prime[j]\) 时,我们也有了一个漂亮的利用积性函数性质而不必特殊构造的计算方法:\(f[k]=f[\frac{i}{last[i]}]\cdot f[last[k]]\) ,只是……
如果 \(k\) 是质数的幂导致 \(last[k]=k\) 怎么办啊!!!
对于具体题目手动构造吧,通常都是很好构造的……
下面举个例子:函数 \(f(x)=\sum_{d|n} d^K\cdot \mu(\frac{x}{d})\) ,其中 \(K\) 是给定的常量(大写是为了方便叙述,防止与代码中的 \(k\) 混淆)。(这个例子出自【BZOJ4407】于神之怒加强版)
当 \(k=p^a\) ,由于当 \(\frac{k}{d}\) 有质数平方因子时 \(\mu(\frac{k}{d})=0\) ,所以只有 \(d=p^a=k\) 和 \(d=p^{a-1}\) 两项对答案有贡献,于是就有 \(f(k)=k^K-p^{(a-1)K}\) 。(很多和 \(\mu\) 有关的函数都可以利用有平方因子的项函数值为 \(0\) 的性质来优化到只有很少的项),代码如下:
void init()
{
f[1] = 1;
for (int i = 2; i < N; i++)
{
if (!mark[i])
prime[cnt++] = last[i] = i, f[i] = (power(i, K) - 1 + p) % p;
for (int j = 0; j < cnt && (ll)i * prime[j] < N; j++)
{
int k = i * prime[j];
mark[k] = true;
if (i % prime[j] == 0)
{
last[k] = last[i] * prime[j];
if (k != last[k])
f[k] = (ll)f[i / last[i]] * f[last[k]] % p;
else
f[k] = (power(k, K) - power(i, K) + p) % p;
break;
}
else
{
last[k] = prime[j];
f[k] = (ll)f[i] * f[prime[j]] % p;
}
}
}
}
杜教筛
杜教筛用于在低于线性的时间(据说是 \(O(n^{\frac{2}{3}})\) ?并不会证)内算出指定积性函数 \(f(x)\) 的前缀和 \(S_f(x)\) 。
(以下对于函数 \(f\) ,定义 \(S_f(x)=\sum_{i=1}^{x}f(i)\) )
杜教筛的主要思想是构造便于计算前缀和的两个函数 \(g(x)\) 和 \(h(x)=(f*g)(x)\) ,然后通过 \(S_g(x)\) 和 \(S_h(x)\) 计算出 \(S_f(x)\) 。具体如下(第三行是先枚举 \(d\) ,然后枚举 \(d\) 的所有倍数,用 \(i\cdot d\) 代替第二行中的 \(i\) ):
之前提到过,构造的 \(S_g(x)\) 和 \(S_h(x)\) 都便于计算(通常是 \(O(1)\) ),所以右边明显可以数论分块,递归计算 \(S_f(\lfloor\frac{n}{d}\rfloor)\) 。
那么现在的问题就是构造合适的函数 \(g\) 。首先,杜教筛经常用来筛 \(\mu\) 和 \(\varphi\) ,而这两个函数都有优美的性质使 \(g\) 的选取十分容易。下面三个式子非常有用,请牢记。我不想证也不会证
(其中 \(\epsilon(x)=\begin{cases}1\ (x=1)\\0\ \mathrm{otherwise}\end{cases}\) , \(\mathrm{id(x)}_k=x^k\) , \(1(x)=1\) )
很显然 \(\epsilon\) 的前缀和恒为 \(1\) ,\(1\) (常函数)的前缀和就是 \(\mathrm{id}\) ,\(\mathrm{id}_1\) 的前缀和就是一个等差数列求和(顺带一提,\(S_{\mathrm{id}_2}(n)=\frac{n(n+1)(2n+1)}{6}\) ),都很好算。所以要求的 \(f=\mu\) 和 \(\varphi\) 时,\(g\) 都选 \(1\) ,对应 \(h\) 分别是 \(\epsilon\) 和 \(\mathrm{id}_1\) 。
其他的构造技巧暂且不提,先看看筛 \(\mu\) 和 \(\varphi\) 函数的代码实现:
const int N067 = 1.67e6 + 10, N033 = 1.3e3 + 10;
typedef long long ll;
ll sumphi[N033], prephi[N067];
int summu[N033], premu[N067], phi[N067], mu[N067], prime[N067], cnt;
bool mark[N067], vis[N033];
void init()
{
phi[1] = mu[1] = 1;
for (int i = 2; i < N067; i++)
{
if (!mark[i])
prime[cnt++] = i, phi[i] = i - 1, mu[i] = -1;
for (int j = 0; j < cnt && (ll)i * prime[j] < N067; j++)
{
int k = i * prime[j];
mark[k] = true;
if (i % prime[j] == 0)
{
mu[k] = 0;
phi[k] = phi[i] * prime[j];
break;
}
else
{
mu[k] = -mu[i];
phi[k] = phi[i] * phi[prime[j]];
}
}
}
prephi[0] = premu[0] = 0;
for (int i = 1; i < N067; i++)
{
prephi[i] = prephi[i - 1] + phi[i];
premu[i] = premu[i - 1] + mu[i];
}
}
typedef pair<ll, int> pli;
pli Du_Algorithm(const int n, const int x)
{//f(i) = phi(i), g(i) = 1, h(i) = i;
//f(i) = mu(i), g(i) = 1, h(i) = \epsilon(i)
if (x < N067)
return make_pair(prephi[x], premu[x]);
int t = n / x;
if (vis[t])
return make_pair(sumphi[t], summu[t]);
pli ans = make_pair((ll)(x + 1) * x / 2, 1);
int pos = 2;
while (pos <= x)
{
int tmp = x / (x / pos);
pli anss = Du_Algorithm(n, x / tmp);
ans.first -= anss.first * (tmp - pos + 1);
ans.second -= anss.second * (tmp - pos + 1);
pos = tmp + 1;
}
vis[t] = true;
return make_pair(sumphi[t] = ans.first, summu[t] = ans.second);
}
(以下这些东西怎么影响复杂度我都不会证)
首先对于 \(n^\frac{2}{3}\) 以内的可以直接线性筛预处理。而对于大于 \(n^\frac{2}{3}\) 的情况,一定要记忆化来保证复杂度。记忆化可以使用 STL 的 map ,也可以把 \(x\) 映射到 \(\lfloor\frac{n}{x}\rfloor\) 。这样做的理由是 \(x\) 只会是 \(n\) 除以某个数下取整的结果,而当 \(x\) 大于 \(n^\frac{2}{3}\) 时一个 \(\lfloor\frac{n}{x}\rfloor\) 只对应一个 \(x\) (并不会证明),所以这样记忆化是对的。
下面介绍一个比较一般的构造 \(g\) 函数的技巧:
首先要知道,狄利克雷卷积有交换律和结合律。它一个重要的性质,类似乘法分配律(注意 \(h\) 必须是完全积性函数):
利用这个性质可以「凑」出一些有用的东西。比如一个看起来很棘手的积性函数 \(f=id_2\cdot(id_5*\mu)\) 。似乎一时想不出什么合适的 \(g\) ,但是……看到有个 \(\mu\) ?
看到 \(\mu\) 自然想到要搞个 \(1\) 来合作出一个 \(\epsilon\) 啊。于是根据上面那个性质,卷上 \(g=id_2\cdot 1\) :
这样就可以把 \(id_2\) 提出来,得到(别忘了狄利克雷卷积有交换律和结合律):
点乘右边就是 \(id_5*\epsilon\) ,手玩一下发现这玩意就是 \(id_5\) (任何函数乘上单位函数 \(\epsilon\) 等于本身)。
于是:
至于 \(id_7\) 的前缀和怎么算?上面那个函数是我瞎写的,我也不知道最后会算出来这种奇怪的东西。所以这玩意的前缀和我也不会算,我去问蹦蹦蹦蹦瓜( jumpmelon )了,再见。
Upd: 蹦蹦瓜说 \(id_k\) 的前缀和是一个 \(k+1\) 次多项式,所以暴力算若干项然后 高斯插值 高斯消元算一下就好了。
Min_25 筛
咕了快三个月后开始更新
这玩意我学了三遍,写博客的时候在 APIO2019 讲课现场掉线后学第四遍(摔.jpg)
注意 Min_25 筛只能求积性函数前缀和。定义 \(p_i\) 表示从小到大第 \(i\) 个质数。特别地, \(p_0=1\) 。
记 \(g(n,m)\) 为 \(\sum f(i)\) ,其中 \(i\) 满足 \(2\leq i\leq n\) 且 \(i\) 的最小质因子大于 \(p_m\) 。则答案为 \(g(n,0)+f(1)=g(n,0)+1\) 。出于某种原因,再设 \(h(n)\) 表示 \(\sum f(p)\) ,其中 \(p\leq n\) 且 \(p\) 是质数(怎么算下来再说)。则:
(我也不知道怎么就套了这么多括号)方括号表示其中的表达式为真则值为 \(1\) ,否则为 \(0\) 。
这个式子的主要思想是:对于符合条件的质数(即大于 \(p_m\) 的质数)单独提出来算(即 \(h(n)-h(p_m)\) );对于合数,枚举它的最小质因子 \(p_i\) 及其指数 \(k\) ,利用积性函数的性质将其拆成 \(p_i^k\cdot a\) 的形式,其中 \(a\) 的最小质因子大于 \(p_i\) 且 \(1\leq a \leq \lfloor\frac{n}{p_i^k}\rfloor\) 。\(a=1\) 时直接算(注意 \(k=1\) 时是质数,在 \(h(n)\) 里算过了),否则递归下去。
接下来的问题是怎么算 \(h(n)\) 。构造 完全积性函数 \(f'(n)\) ,使得当 \(p\) 是质数 时 \(f'(p)=f(p)\) (怎么构造稍后再说)。再设一个函数 \(z(n,m)\) 表示 \(\sum f'(i)\) ,其中 \(i\leq n\) 且 \(i\) 的最小质因子大于 \(p_m\) 或 \(i\) 是质数,则 \(h(n)=z(n,\omega(\sqrt{n}))\) ,其中 \(\omega(\sqrt{n})\) 表示不超过 \(\sqrt{n}\) 的质数个数(毕竟对于质数来说 \(f\) 和 \(f'\) 是一回事,最小质因子超过 \(\sqrt{n}\) 的只有质数了)。考虑从 \(m-1\) 递推出 \(m\) ,即从 \(z(n,m-1)\) 中减去最小质因子为 \(p_m\) 的数的贡献:
注意,根据定义,\(z\left(\lfloor \frac{n}{p_m}\rfloor,m-1\right)\) 中不仅包含(我们需要的)不超过 \(\lfloor \frac{n}{p_m}\rfloor\) 且最小质因子大于 \(p_{m-1}\) 的数的贡献,还包括所有不超过 \(\lfloor \frac{n}{p_m}\rfloor\) 的质数的贡献。在第二部分中, \(\left[p_m,\lfloor\frac{n}{p_m}\rfloor\right]\) 中的质数也满足第一个条件(即是两部分的交集),所以无需减去。要减去的是小于 \(p_m\) 的质数的贡献,即 \(z(p_m-1,m-1)\) 。
现在只剩下最后一个问题了:如何构造 完全积性函数 \(f'(n)\) 。通常来说,当 \(p\) 是质数时 \(f(p)\) 是关于 \(p\) 的多项式,如 \(\mu(p)=-1\),\(\varphi(p)=p-1\) 等。而 \(id_k\) ( \(k\in N\) , \(id_k(n)=n^k\) ) 都是完全积性函数,所以可以分别对每项用不同的 \(id_k\) 作为 \(f'\) 求出 \(h\) ,然后乘上对应系数加起来即可。以 \(\varphi\) 为例,可以用 \(f'_1(i)=i\) 算出 \(z_1\) ,用 \(f'_2(i)=1\) 算出 \(z_2\) ,最后 \(z=z_1+(-1)\cdot z_2\) 。