《算法笔记》——第五章 素数 学习记录

素数又称为质数,是指除了1和本身之外,不能被其他数整除的一类数。

即对给定的正整数n,如果对任意的正整数a(1<a<n),都有n%a!=0成立,那么称n是素数;否则,如果存在a(1<a<n),使得n%a=0,那么称n为合数。

应特别注意的是,1既不是素数,也不是合数。

本节将解决两个问题:

  1. 如何判断给定的正整数n是否是质数;
  2. 如何在较短的时间内得到1~n内的素数表。

素数的判断

从素数的定义中可以知道,一个整数要被判断为素数,需要判断n是否能被2,3,...,n-1中的一个整除。只有2,3,...,n-1都不能整除n,n才能判定为素数,而只要有一个能整除n的数出现,n就可以判定为非素数。

这样的判定方法没有问题,复杂度为\(O(n)\)。但是在很多题目中,判定素数只是整个算法中的一部分,这时候\(O(n)\)的复杂度实际上有点大,需要更加快速的判定方法。

注意到如果在2~n-1中存在n的约数,不妨设为k,即n%k==0,那么由k*(n/k)=n可知,n/k也是n的一个约数,且k与n/k中一定满足其中一个小于等于\(\sqrt{n}\)、另一个大于等于\(\sqrt{n}\)

这启发我们,只需要判定n能否被\(2, 3, ... ,\lfloor \sqrt{n} \rfloor\)中的一个整除,即可判定n是否为素数。该算法的复杂度为\(O(\sqrt{n})\)

bool isPrime(int n)
{
    if(n<2) return false;
    for(int i=2;i*i<=n;i++)
        if(n % i == 0) 
            return false;
    return true;
}

这样写会当n接近int型变量的范围上界时导致i*i溢出(当然n在\(10^9\)以内都会是安全的),解决的办法是将i定义为long long型,这样就不会溢出了。

素数表的获取

通过上面的学习,读者应已有办法判断单独一个数是否是素数,那么可以直接由此得出打印1n范围内素数表的方法,即从1n进行枚举,判断每个数是否是素数,如果是素数则加入素数表。这种方法的枚举部分的复杂度是\(O(n)\),而判断素数的复杂度是\(O(\sqrt{n})\),因此总复杂度是0(n√n)。这个复杂度对n不超过\(10^5\)的大小是没有问题的。

上面的算法对于n在10以内都是可以承受的,但是如果出现需要更大范围的素数表,\(O(n \sqrt{n})\)的算法将力不从心。下面学习一个更高效的算法,它的时间复杂度为\(O(nloglogn)\)

“埃式筛法”是众多筛法中最简单且容易理解的一种,即Eratosthenes筛法。更优的欧拉筛法可以达到\(O(n)\)的时间复杂度,此处不予赘述。

素数筛法的关键就在一个“筛”字。算法从小到大枚举所有数,对每一个素数, 筛去它的所有倍数,剩下的就都是素数了。可能有读者问,一开始并不知道哪些数是素数,何来的“对每一个素数”呢?下面来看一一个例子:求1~ 15中的所有素数。

  1. 2是素数(唯一需要事先确定),因此筛去所有2的倍数,即4、6、8、10、12、14。
  2. 3没有被前面的步骤筛去,因此3是素数,筛去所有3的倍数,即6、9、12、15。
  3. 4已经在①中被筛去,因此4不是素数。
  4. 5没有被前面的步骤筛去,因此5是素数,筛去所有5的倍数,即10、15。
  5. 6已经在①中被筛去,因此6不是素数。
  6. 7没有被前面的步骤筛去,因此7是素数,筛去所有7的倍数,即14。
  7. 8已经在①中被筛去,因此8不是素数。
  8. 9已经在②中被筛去,因此9不是素数。
  9. 10已经在①中被筛去,因此10不是素数。
  10. 11没有被前面的步骤筛去,因此11是素数,筛去所有11的倍数,但在15以内没有。
  11. 12已经在①中被筛去,因此12不是素数。
  12. 13没有被前面的步骤筛去,因此13是素数,筛去所有13的倍数,但在15以内没有。
  13. 14已经在⑥中被筛去,因此14不是素数。
  14. 15已经在②中被筛去,因此15不是素数。
    至此,1~15 内的所有素数已全部得到,即2、3、5、7、11、13。

由上面的例子可以发现,当从小到大到达某数a时,如果a没有被前面步骤的数筛去,那么a一定是素数。这是因为,如果a不是素数,那么a一定有小于a的素因子,这样在之前的步骤中a一定会被筛掉,所以,如果当枚举到a时还没有被筛掉,那么a一定是素数。

可以证明筛法的复杂度为\(O(\sum_p\frac{n}{p})=O(n\log \log n)\)

当然,上面的做法效率仍然不够高效,可以稍微提高算法的执行效率。

    for(int i=2;i*i<=n;i++)
        if(!vis[i])
            for(int j=i*i;j<=n;j+=i) vis[j]=true;
            // 因为从 2 到 i - 1 的倍数我们之前筛过了,这里直接从i的倍数开始,提高了运行速度

用来做筛除的数最多到\(\sqrt{n}\)就可以了。其原理和试除法一样:非素数k必定可以被一个小于等于\(\sqrt{k}\)的素数整除。

如果需要记录所有素数,那么还需要\(O(n)\)的时间扫描一遍vis数组,所以对于需要记录素数表的题目一般采取如下写法:

    for(int i=2;i<=n;i++)
        if(!vis[i])
        {
            primes[cnt++]=i;
            for(int j=i;j<=n/i;j++)
                vis[i*j]=true;
        }

内层循环为防止i*i溢出换成了不会溢出的等价写法。

两份代码的渐进时间复杂度相同,第一份代码的操作次数会明显减少,但无法在筛的同时记录下所有素数,只能记录到\(\sqrt{n}\)内的素数。

质因子分解

所谓质因子分解是指将一个正整数n写成一个或多个质数的乘积的形式,例如6=2x3,8=2x2x2,180=2x2x3x3x5。或者我们也可以写成指数的形式,例如\(6=2^1 \times 3^1,8=2^3,180=2^2 \times 3^2 \times 5^1\)

显然,由于最后都要归结到若干不同质数的乘积,因此不妨先把素数
表打印出来。而打印素数表的方法上面已经阐述,下面我们主要就质因子分解本身进行讲解。

注意:由于1本身不是素数,因此它没有质因子,下面的讲解是针对大于1的正整数来说的,而如果有些题目中要求对1进行处理,那么视题目条件而定来进行特判处理。

由于每个质因子都可以不止出现一次,因此不妨定义结构体factor,用来存放质因子及个数,如下所示:

struct factor 
{
	int x,cnt;
}fac[10];

考虑到2x3x5x7x11x13x17x19x23x29就已经超过了int范围,因此对一个int型范围的数来说,fac数组的大小只需要开到10就可以了

前面提到过,对一个正整数n来说,如果它存在1和本身之外的因子,那么一定是在\(\sqrt{n}\)的左右成对出现。而这里把这个结论用在“质因子”上面,会得到一个强化结论:对一个正整数n来说,如果它存在[2, n]范围内的质因子,要么这些质因子全部小于等于\(\sqrt{n}\),要么只存在一个大于\(\sqrt{n}\)的质因子(若有两个,则相乘后大于\(n\)),而其余质因子全部小于等于\(\sqrt{n}\)。这就给进行质因子分解提供了一个很好的思路:

  1. 枚举\(1 \sim \sqrt{n}\)范围内的所有质因子p,判断p是否是n的因子。
    • 如果p是n的因子,那么给fac数组中增加质因子p,并初始化其个数为0。然后,只要p还是n的因子,就让n不断除以p,每次操作令p的个数加1,直到p不再是n的因子为止。
    • 如果p不是n的因子,就直接跳过。
  2. 如果在上面步骤结束后n仍然大于1,说明n有且仅有一个大于\(\sqrt{n}\)的质因子(有可能是n本身),这时需要把这个质因子加入fac数组,并令其个数为1。

至此,fac 数组中存放的就是质因子分解的结果,时间复杂度是\(O(\sqrt{n})\)

map<int,int> fac;

void divide(int n)
{
    for(int i=2;i*i<=n;i++)
        if(n % i == 0)
            while(n % i == 0)
            {
                fac[i]++;
                n/=i;
            }
        
    if(n > 1) fac[n]++;
}

\(map\)会按照质因子大小升序存储,若不要求顺序可采用\(unordered_map\)

最后指出,如果要求一个正整数N的因子个数,只需要对其质因子分解,得到各质因子\(p_i\)的个数分别为\(e_1,e_2, \cdots e_k\),于是\(N\)的因子个数就是\((e_1+1)*(e_2+1)* \cdots *(e_k+ 1)\)。原因
是,对每个质因子\(p_i\)都可以选择其出现\(0\)次、\(1\)次、\(\cdots\)\(e_i\)次,共\(e_i+1\)种可能,组合起来就是答案。

而由同样的原理可知,\(N\)的所有因子之和为

\[(1+p_1+p_1^2+\cdots+p_1^{e_1})*(1+p_2+p_2^2+\cdots+p_2^{e_2})*\cdots(1+p_k+p_k^2+\cdots+p_k^{e_k}) \\ =\frac{1-p_1^{e_1+1}}{1-p_1}*\frac{1-p_2^{e_2+1}}{1-p_2}*\cdots*\frac{1-p_k^{e_k+1}}{1-p_k} \]

posted @ 2021-02-12 18:34  Dazzling!  阅读(25)  评论(0编辑  收藏  举报