Files
python/TangDou/AcWing/DP/Tree/线性筛约数个数和与约数和.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

257 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 线性筛求 <font color='blue'>约数个数</font> 与 <font color='blue'>约数和</font>
线性筛,顾名思义,就是欧拉筛,在线性时间内可以求出答案,也就是$O(N)$的时间,非常牛$X$的效率。
### 一、约数个数
根据数字唯一分解定理,设
$$\LARGE n=p_1^{r_1}*p_2^{r_2}*p_3^{r_3}*...*p_k^{r_k}$$
对于每个$n$的约数,一定是由以上质因数$p_1 \sim p_k$相乘而来,根据乘法原理,每个质因数都可以选择$0$到$r_i$这$r_i+1$个选择。
$n$的约数个数即为
$$\LARGE d(n)=(r_1+1)\times (r_2+1) \times ... \times (r_k+1) =\prod_{i=1}^k(r_i+1)$$
**筛的过程中需要保存$n$的最小质因子的出现个数即$r_1$,原因后面会说明:**
<font color='red' size=4><b>
* $d[i]$记录$i$的约数个数
* $num[i]$记录$i$的最小质因数个数
</b></font>
分情况讨论:
#### (一)、$i$是质数
$$
\large \left\{\begin{matrix}
d[i]=2 & \\
num[i]=1 &
\end{matrix}\right.
$$
* $d[i]$:$i$是质数,所以约数只有$1$和自身,约数个数为$2$
* $num[i]$:$i$是质数,最小质因子就是自己,个数是$1$
#### (二)、$i$取模枚举的第$j$个质数为$0$, $primes[j]$整除$i$
<font color='blue' size=4><b>$Q1:$我们在研究哪些数字之间的递推关系?</b></font>
$A:$既然是递推,一般都是已知小的推出大的,这里应该是已知$d[i]$推出$d[i*primes[j]]$的值。
<font color='blue' size=4><b>$Q2$:$d[i*primes[j]]$是啥?</b></font>
$A$: <font color='red' size=4><b>
$primes[j]$是$i$的因子,$i*primes[j]$唯一分解不会产生新的质因子,依然是$p_1,p_2,...p_k$
$primes[j]$从小到大枚举,所以一定是$i$的最小质因子!所以$p_1^{r_1+1}$,按唯一分解定理展开:
$$\large d[i*primes[j]]=(1+r_1+1)*......*(1+r_k)$$
</b></font>
<font color='blue' size=4><b>$Q3$:$d[i∗primes[j]]$怎么从$d[i]$转移?</b></font>
这个时候就可以用到我们之前维护的$num[i]$了。
转移也就非常简单了:
$$\large d[i∗primes[j]]=d[i]/(num[i]+1)∗(num[i]+2)$$
<font color='red' size=4><b>解释:
$$
\large \left\{\begin{matrix}
d(i)=(1+r_1)*......*(1+r_k) & ①\\
d(i*primes[j])=(1+r_1+1)*......*(1+r_k) & ②\\
\end{matrix}\right.
$$
使用瞪眼大法(观察法),找出两者之间的递推关系,很明显,就是 ①式除以$1+r_1$,再乘上$1+r_1+1$即可转化为 ②式,这下明白为什么要记录$num[i]$最小质因子的个数了吧~
</b></font>
<font color='blue' size=4><b>$Q4$:那么$num[i∗primes[j]]$是不是也需要从$num[i]$进行转移呢?</b></font>
$A$:$num[i*primes[j]]$也要转移,加上最小质因子$primes[j]$的贡献也就是:
$$\large num[i∗primes[j]]=num[i]+1$$
#### (三)、当前数取模枚举的第$j$个质数不为$0$,即 $primes[j]$ 不能整除 $i$
首先我们知道$i∗primes[j]$这个数中之前一定不包含$primes[j]$这个质因数,
那么约数个数要加上$prime[j]$的,也就是:
$$\large d[i*primes[j]]=(1+r_1)*...*(1+r_k)*(1+1) \\
=d[i]*2=d[i]*d[primes[j]] $$
然后对于最小质因子,因为$j$是从小到大枚举的,所以$i∗primes[j]$这个数的最小质因子也就是$primes[j]$
所以就可以得到:
$$\large num[i*primes[j]]=1$$
综上,就可以写出筛质因数个数的代码了:
```c++
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 1010;
int primes[N], cnt; // primes[]存储所有素数
bool st[N]; // st[x]存储x是否被筛掉
int d[N]; // d[x]表示x的约数个数
int num[N]; // num[x]表示x的最小质因数的个数
int n;
//欧拉筛法+求约数个数
void get_primes(int n) {
d[1] = 1; // 1的约数只有1个,这个比较特殊
for (int i = 2; i <= n; i++) {
if (!st[i]) {
primes[cnt++] = i;
// i是质数
d[i] = 2; //约数个数是2个,一个是1,另一个是i
num[i] = 1; //最小质因子个数是1,最小质因子就是自己i
}
for (int j = 0; i * primes[j] <= n; j++) {
st[i * primes[j]] = true;
if (i % primes[j] == 0) {
d[i * primes[j]] = d[i] / (num[i] + 1) * (num[i] + 2);
num[i * primes[j]] = num[i] + 1;
break;
} else {
// d[i * primes[j]] = d[i] * d[primes[j]]; 等价于下面的代码 
d[i * primes[j]] = d[i] * 2;
num[i * primes[j]] = 1;
}
}
}
}
int main() {
scanf("%d", &n);
get_primes(n);
//输出1~n之间所有数字的约数个数
for (int i = 1; i <= n; i++) printf("%d %d\n", i, d[i]);
return 0;
}
```
### 二、约数和
<font color='red' size=4><b>设$sd[i]$表示$i$的约数和</b></font>
根据算数基本定理得:[约数和参考链接](https://www.cnblogs.com/littlehb/p/15341428.html)
$$\large sd[n]=(p_1^0+p_1^1+p_1^2+……+p_1^{r_1})∗(p_2^0+p_2^1+p_2^2+……+p_2^{r_2})∗……∗(p_k^0+p_k^1+p_k^2+……+p_k^{r_k})$$
那么根据这个式子就可以开始干了。。。
<font color='red' size=4><b>
设$num[i]$表示最小质因子的那一项:
$$\large num[i]= p_1^0+p_1^1+p_1^2+……+p_1^{r_1}$$
</b></font>
分情况讨论:
#### (一)、$i$是质数
$$\large sd[i]=num[i]=i+1$$
<font color='red' size=4><b>解释:
* $i$是质数,它的约数只有自己和$1$,约数和是$i+1$
* $i$是质数,质因子分解也只能分解出自己,$sd[i]=i^0+i^1=1+i$
</b></font>
#### (二)、$i$取模枚举的质数$primes[j]$不为0,即$primes[j]$不能整除$i$
<font color='red' size=4><b>注:下面的讨论中使用$p_j$代替$primes[j]$</b></font>
$i∗p_j$里原先没有$p_j$这一项,加上这一项之后:
$$\large \because sd[i]=(p_1^0+p_1^1+p_1^2+……+p_1^{r_1})∗(p_2^0+p_2^1+p_2^2+……+p_2^{r_2})∗……∗(p_k^0+p_k^1+p_k^2+……+p_k^{r_k})$$
$$\large \because sd[i*p_j]=(p_1^0+p_1^1+p_1^2+……+p_1^{r_1})∗(p_2^0+p_2^1+p_2^2+……+p_2^{r_2})∗……∗(p_k^0+p_k^1+p_k^2+……+p_k^{r_k})*(p_j^0+p_j^1)$$
$\large \because p_j$ 是质数 $\large \therefore sd[p_j]=p_j^0+p_j^1$
$\large \therefore sd[i*p_j]=sd[i]*sd[p_j]$ <font color='red' size=4><b>注:积性函数</b></font>
**同时更新一下$num[i∗p_j]$**
$$\large num[i∗p_j]=num[p_j]=p_j^0+p_j^1=1+p_j$$
<font color='red' size=4><b>解释:
因为质因子从小到大枚举,那么$i∗p_j$的最小质因子就应该是$p_j$,那么$num[i∗p_j]$也就应该等于$num[p_j]$
</b></font>
#### (三)、$i$取模枚举的质数$j$为0,即 $primes[j]$整除$i$
那么$sd[i∗p_j]$中的第一项也就是$num[i∗p_j]$也一定是$p_j$的第一项
<font color='red' size=4><b>解释:(等比数列变形小技巧)</b></font>
$1+p_i+p_i^2+……+p_i^{r_i}$这个时候要变成$1+p_i+p_i^2+……+p_i^{r_i}+p_i^{r_i+1}$,那么只需要所有的都乘以一个$p_i$,然后再加一个$1$就好了。
<font color='red' size=4><b>
$$\large (1+p_i+p_i^2+……+p_i^{r_i})*p_i+1= \\
1+p_i+p_i^2+……+p_i^{r_i}+p_i^{r_i+1}$$
</b></font>
**结论:**
$$\large num[i∗p_j]=num[i]∗p_j+1$$
这样,我们又可以开始愉快的写代码啦...
```c++
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e3 + 10;
int primes[N], cnt;
bool st[N];
int num[N]; //最小质因子p1组成的等比序列 p1^0+p1^1+...+p1^r1
int sd[N]; //约数和
int n;
void get_primes(int n) {
sd[1] = 1; // 1的约数只有自己,约数和是1
for (int i = 2; i <= n; i++) {
if (!st[i]) {
primes[cnt++] = i;
//质数
sd[i] = num[i] = i + 1; // 1和自身是约数,约数和为i+1; 同时因为i是质数,所以只有一个分拆项,并且分拆项内容=pj^0+pj^1=1+pj=1+i
}
for (int j = 0; primes[j] * i <= n; j++) {
st[i * primes[j]] = true;
if (i % primes[j]) {
sd[i * primes[j]] = sd[i] * sd[primes[j]]; //积性函数
num[i * primes[j]] = primes[j] + 1;
} else {
sd[i * primes[j]] = sd[i] / num[i] * (num[i] * primes[j] + 1);
num[i * primes[j]] = num[i] * primes[j] + 1;
break;
}
}
}
}
int main() {
scanf("%d", &n);
get_primes(n);
//约数和要不要自己本身,这里是不想要自己本身
for (int i = 1; i <= n; i++) printf("%d ", sd[i] - i);
puts("");
return 0;
}
```