【简单数论】质数和约数

本文简要讲解简单数论问题:质数和约数相关问题。包括以下内容:

  1. 质数相关:判定质数、分解质因数、筛质数
  2. 约数相关:求约数、约数个数、约数之和、最大公约数

1、质数(素数)

先看一下质数的定义:质数 ( Prime number ),又称 素数 ,指在大于 1 的 自然数 中,除了1和该数自身外,无法被其他自然数 整除 的数(也可定义为只有1与该数本身两个正因数的数)。 大于1的自然数若不是质数,则称之为 合数 (也称为合成数)。小于等于1的数既不是质数,也不是合数。

举个例子:2的因子为1和2,所以为质数;6的因子为1、2、3、6,所以为合数;17的因子为1和17,所以为质数。

关于质数,有一些定理需要掌握:

  1. 质数定理:1~n中有 n/logn个质数;
  2. 唯一分解定理:任何一个大于1的正整数n都可以唯一分解成有限个质数的乘积,这些数叫n的质因子

还有一些性质

  1. 任何一个大于1的正整数一定存在一个最小质因子
  2. 一个大于1的正整数 N 最多只包含一个大于 n \sqrt n n 质因子,且其幂次一定是1(假设有两个a, b,那么 a*b 一定大于 n )。

此外,还有一个易混淆的概念:整除。若整数b除以非零整数a,商为整数,且 余数 为零,我们就说b能被a整除(或说a能整除b),b为被除数,a为除数,即a|b(“|”是整除符号),读作“a整除b”或“b能被a整除”。a叫做b的约数(或因数),b叫做a的倍数。

1.1、判定质数

想要判定一个数n是不是质数,首先质数一定是整数,其次小于2的数直接被排除,剩下的数用试除法,也即从2到n-1一个一个试看能不能整除n。同时注意到一个性质:如果a|b,那么(b/a)|b,即一个数的约数都是成对出现的。所以其实只需要从2遍历到 n \sqrt n n 即可。这样优化让时间复杂度变为O( n \sqrt n n )了。那么给出函数代码(适用于C/C++):

//输入一个x,如果是质数就返回真,如果不是就返回假
int is_prime(int x)
{
    if (x < 2) return 0;
    //这里有其他写法:
    //1. i * i <= x  有溢出风险
    //2. i <= sqrt(x)  每次都要算sqrt,很慢
    //3. i * i <= x 也即 i <= x / i  最优写法
    for (int i = 2; i <= x / i; i ++ )
        if (x % i == 0)
            return 0;
    return 1;
}

1.2、分解质因数

分解n的质因数即把n表示成有限个质数乘积的形式。只需要从小到大枚举所有数即可,枚举到一个n的因子a,就用n连续除以a,直到a不是当前数的因子。

那么这里会有一个问题:为什么不是枚举所有质因数呢?其实只要从小到大枚举所有数,能除的都除掉,就能保证枚举到的一定是质数。简单证明一下:假设枚举到了a,那么a的因子一定是n的因子,所以a的质因子一定是n的质因子,但是2 ~ a-1的质因子已经被除干净了,所以a一定没有2 ~ a-1的质因子了,所以a一定是质数。

此外要注意,除到最后可能会剩一个大于 n \sqrt n n 的质因子,这时候特判一下输出即可。

综上,直接给出代码(适用于C/C++):

//输入一个数,给出分解的质因子+幂次
//比如分解6
//那么输出
//2(质因子) 1(幂次)
//3(质因子) 1(幂次)
void divide(int x)
{
    for (int i = 2; i <= x / i; i ++ )
    {
        if (x % i == 0)
        {
        	//s记录幂次
            int s = 0;
            //把这个质因子除干净
            while (x % i == 0) x /= i, s ++ ;
            printf("%d %d\n", i, s);
        }
    }
    //这个大于sqrt(x)的质因子幂次一定是1
    if (x > 1) printf("%d 1\n", x);
    printf("\n");
}

最坏情况是一个数刚好是2的幂,那么需要进行logn次,所以这个算法的时间复杂度是O(logn)的,但实际平均下来,是要远小于logn的,因为一个数很小概率是2的幂。

1.3、筛质数

1.3.1、埃氏筛

要求是给定一个数n,求出1~n之间所有的质数。正常思路是从2到n一个一个判断,但是这未免太愚蠢了。所以要进行优化,优化的方法是每枚举到一个质数,就把它的倍数全部筛掉。假设枚举到a且a没有被筛掉,那么a一定没有2 ~ a-1之间的质因子了,因为2 ~ a-1之间所有的质因子的倍数都被筛掉了,那么a一定是质数。因为这个算法是古希腊数学家埃拉托色尼发明的,所以叫埃氏筛。
所以可以直接给出代码了(适用于C/C++):

//primes数组记录质数,cnt记录下标(也即有几个质数)
int primes[N], cnt;
//st数组记录当前数有没有被筛掉
int st[N];

//输入n,求得1~n之间所有质数,并存入primes数组
void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
    	//如果标记为被筛掉,就跳过当前数
    	//每个数都会被质因子筛掉,所以如果是合数就直接跳过即可
        if (st[i]) continue;
        //如果没有被筛,那么就加入质数数组
        primes[cnt ++ ] = i;
        //用当前数来筛掉所有倍数
        for (int j = i + i; j <= n; j += i) st[j] = 1;
    }
}

这个算法的时间复杂度为O(nloglogn),基本和O(n)一样。因为推导过于复杂,这里就不赘述了。

1.3.2、欧拉筛(线性筛)

对于埃氏筛,注意到一个问题:一个数可能被筛多次,比如6会被2筛一次,还会被3筛一次。那么要优化这个问题,我们使用欧拉筛(即线性筛):让每个数只被最小质因子筛掉,这样保证每个数只被筛一次,大大提高了效率。
欧拉筛也是依次枚举每个数,枚举到i的时候,如果i是没被筛,就存入数组;否则就不存入数组。然后依次枚举从小到大的质数x,并筛掉i * x。直到 x 是 i 的最小质因子。这样能保证每个数只会被筛一次(参考唯一分解定理)。

下面回答一下难以理解的一些问题:

  1. 为什么每个合数是被最小质因子筛掉?筛的时候有两种情况:第一种是i%x为0,那么此时x一定是i的最小质因子(因为是从小到大枚举所有质数),那么x也一定是x * i的最小质因子;第二种是i%x不为0,那么此时x一定小于i的所有质因子(因为是从小到大枚举所有质数),那么x一定就是x * i的最小质因子。综上所述,每个数都是被最小质因子筛掉的。
  2. 为什么每个合数一定会被筛掉?当枚举到某一个合数i时,它一定有一个最小质因子x,而i/x一定小于i,所以它会在之前枚举到x/i的时候被筛掉
  3. 为什么到 x 是 i 的最小质因子就停了?因为如果再往下筛,假设下一个质数是y,那么y一定大于x(因为是从小到大枚举质数),但是y * i的最小质因子是x,所以y * i就不是被最小质因子筛掉的了
  4. 暂停条件不应该加上质数数组的边界吗?如果i是质数,那么一定会在枚举到i的时候停,因为质数的最小质因子是本身;如果i是合数,那一定会在枚举到i的最小质因子时停,i的最小质因子一定小于i,所以i的最小质因子一定已经加入质数数组了。综上,即使不加上越界条件,也会停下。

下面给出代码(适用于C/C++):

int primes[N], cnt;
int st[N];

//输入n,求得1~n之间所有质数,并存入primes数组
void get_primes(int n)
{
	//依次枚举所有数
    for (int i = 2; i <= n; i ++ )
    {
    	//如果没被筛掉,就加入素数数组
        if (!st[i]) primes[cnt ++ ] = i;
        //从小到大枚举所有质数
        //既然最后都会停,为什么会加上primes[j] <= n / i?
        //因为不加的话,就会筛到需求以外的数,一方面增加了时间复杂度,另一方面可能越界
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
        	//筛数
            st[primes[j] * i] = 1;
            if (i % primes[j] == 0) break;
        }
    }
}

2、约数

在整除的概念中,已经介绍了约数的概念,这里就不再赘述。下面直接进入常考问题

2.1、求约数

要求一个数n的约数,也是使用试除法,即从1开始尝试,一直到试到n,如果能整除n,那么就输出。在质数问题中已经提过:一个数的约数都是成对出现的,所以枚举的时候也只要枚举到 n \sqrt n n 即可。只需要特判一下一对约数相等时即可(即约数为 n \sqrt n n )。
下面给出代码(适用于C++,C可以用数组存,并自己写排序函数):

//输入一个数x,返回从小到大存储其约数的vector数组
vector<int> get_divisors(int x)
{
	//存储答案
    vector<int> res;
    for (int i = 1; i <= x / i; i ++ )
    {
        if (x % i == 0)
        {
            res.push_back(i);
            //如果这对约数不是相等的,就加入一对中的另一个
            if (i != x / i) res.push_back(x / i);
        }
    }
    //从小到大排个序
    sort(res.begin(), res.end());
    
    return res;
}

2.2、求约数个数

给定一个数,现在要求它的约数个数。这里要用到一个公式
在这里插入图片描述
简单证明一下
在这里插入图片描述
如果题目给出的是让你求一个比较小的数的约数个数,那其实直接试除法就能解决。但是有这个公式,所以出题人一定会出一些险恶的题目来逼迫你使用这个公式。下面就来看这样一道题目:
原题acwing 约数个数

给定 n 个正整数 a i a_i ai,请你输出这些数的乘积的约数个数,答案对 109+7 取模。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个整数 a i a_i ai
输出格式
输出一个整数,表示所给正整数的乘积的约数个数,答案需对 109+7 取模。
数据范围
1≤n≤100,
1≤ a i a_i ai≤2×109
输入样例

3
2
6
8

输出样例

12

如果用试除法来做,整数直接爆掉了。那么只能用公式来做了:

#include <iostream>
#include <cstdio>
#include <unordered_map>

using namespace std;

typedef long long LL;

const int mod = 1e9 + 7;

int main()
{
    int n;
    cin >> n;
    
    //用stl中的哈希表来存每个质因数以及幂次
    unordered_map<int, int> primes;
    while (n -- )
    {
        int x;
        cin >> x;
        
        //分解质因数
        for (int i = 2; i <= x / i; i ++ )
            while (x % i == 0)
            {
                x /= i;
                primes[i] ++ ;
            }
            
        if (x > 1) primes[x] ++ ;
    }
    
    LL res = 1;
    
    //套公式
    for (auto prime : primes) res = res * (prime.second + 1) % mod;
    
    cout << res << endl;
    
    return 0;
}

2.3、求约数之和

给定一个数,现在要求它的约数之和,要用到另一个公式
在这里插入图片描述
可以简单证明一下:把上式展开,能得到一个多项式,这个多项式包括了 p 1 p_1 p1 ~ p k p_k pk所有幂次的排列组合,而n的每个约数又都是 p 1 p_1 p1 ~ p k p_k pk一种幂次的排列组合,又根据唯一分解定理,每种排列组合都对应一个不同的数,所以这个多项式就包括了n的所有约数,加在一起也就是约数之和。具体大家可以推导一下,证明非常容易。

同样,出题人会出一些阴险的题目来逼迫你使用这个公式:
原题acwing 约数之和

给定 n 个正整数 a i a_i ai,请你输出这些数的乘积的约数之和,答案对 109+7 取模。
输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个整数 a i a_i ai
输出格式
输出一个整数,表示所给正整数的乘积的约数之和,答案需对 109+7 取模。
数据范围
1≤n≤100,
1≤ a i a_i ai≤2×109
输入样例

3
2
6
8

输出样例

252

#include <iostream>
#include <unordered_map>

using namespace std;

typedef long long LL;

const int mod = 1e9 + 7;

int main()
{
    int n;
    cin >> n;
    
    unordered_map<int, int> primes;
    while (n -- )
    {
        int x;
        cin >> x;
        
        for (int i = 2; i <= x / i; i ++ )
            while (x % i == 0)
            {
                primes[i] ++ ;
                x /= i;
            }
            
        if (x > 1) primes[x] ++ ;
    }
    
    LL res = 1;
    //套公式
    for (auto prime : primes)
    {
        LL a = prime.first, b = prime.second;
        LL t = 1;
        //求一个数的连续幂次多项式有很多种方法,这里是一个简答的方法
        //如果不懂可以自己用纸和笔模拟一下
        while (b -- ) t = (t * a + 1) % mod;
        res = res * t % mod;
    }
    
    cout << res << endl;
    
    return 0;
}

2.4、求两个数的最大公约数

求两个数的最大公约数一般使用欧几里得算法(也叫辗转相除法)。首先要掌握一个性质

  1. 如果d|a,d|b,那么d|(ax+by),(x,y是整数)
  2. a和b的最大公约数等于b和a mod b的最大公约数

对于性质2,简单证明一下:
在这里插入图片描述
那么就可以给出代码了(适用于C/C++):

//输入a和b,返回其最大公约数
int gcd(int a, int b)
{
	//如果b不是0,那么就返回b,和a % b的最大公约数
	//如果b是0,那就返回a,因为0能被任何数整除
    return b ? gcd(b, a % b) : a;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值