算法学习模板——素数问题、费马小定理、LCM/GCD和欧拉降幂

2023-05-16

        万里之行,始于足下。本博客总结近期学习到的部分数论模板,以便于日后查询使用。作者水平有限,难免存在疏漏不足,恳请诸位看官斧正。倘若我的文章可以帮到你,十分荣幸。当然,以后随着我的阅历知识增长还敬请期待后序更新。本次内容的灵感来自于lyh学长和lyk学长的讲课和近期的牛客竞赛补题。

目录

1.素数筛

(1)何谓素数?

(2)朴素筛法

(3)埃氏筛(sieve of Eratosthenes)

(4)欧拉筛(sieve of euler)

2.费马小定理(Fermat's little theorem)

(1)何谓费马小定理?

(2)费马小定理求逆元

(3)线性逆元的讨论

3.lcm与gcd

(1)最大公因数(gcd)

(2)最小公倍数(lcm)

4.欧拉降幂


更新日志:

1.于2022.4.16修正了朴素筛法的代码部分。

1.素数筛

(1)何谓素数?

       总结方法前我们不妨先回溯一下知识点:什么是素数?素数又名质数,百度百科对它的定义是:质数是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。

        注意:0和1不是素数!

        这里有素数的两个定理,在后面的解题中比较重要:

        唯一分解定理:任何一个大于1的自然数n,那么n可以唯一分解成有限个不相同的质数的乘积p1^a1 * p2^a2 * ... * pn^an(an为正整数)。

        素数分布规律:素数的分布是越往后越稀疏的,一般来说区间内的素数个数是小于等于区间长度的1/3的。

        一个数是不是素数是好判断的。但是计算机的问题规模往往会成千上万。怎么高效地找出一个范围内的素数?接下来会介绍三种筛法,如沙中淘金,找出一个范围内的素数。

(2)朴素筛法

高情商:朴素     低情商:暴力 

        一个一个枚举区间里的数,利用素数的定义判断枚举到的数是不是素数。

bool isprime(int x){
    if(x<2) return false;//0和1不是素数也不是合数哦!
    for(int i=2;i*i<=x;i++){
        if(x%i==0) return false;
    }
    return true;
}

       嗯哼?为什么是i * i <= x而不是i <= x?因为一个数的最大因子是不会超过它的二次根号的。这是一个小小的细节优化。

(3)埃氏筛(sieve of Eratosthenes)

       朴素筛法简单易懂,但是在处理大量数据时效率较低。在这里介绍埃拉托斯特尼筛法,简称埃氏筛或爱氏筛,是一种由希腊数学家埃拉托斯特尼所提出的一种简单检定素数的算法。要得到自然数n以内的全部素数,必须把不大于根号n的所有素数的倍数剔除,剩下的就是素数。时间复杂度为O(n*log(log n))
bool numlist[maxn];//标记这个数是不是素数;maxn根据要求的范围确定
vector<int> prime;//vector容器存放求到的素数
void Eratosthenes(int n)
{
    memset(numlist,true,sizeof (numlist));
    numlist[0]=numlist[1]= false;//0和1不是素数哦
    for(int i=2;i<=n;i++)
    {
        if(numlist[i])
        {
            prime.push_back(i);//放入vector容器
            for(int j=i;i*j<=n;j++)
            {
                numlist[i*j]= false;//素数的倍数不是素数
            }
        }
    }
}

       不难发现,埃氏筛法在枚举k的每一个质因子时,都计算了一次k,造成了重复筛除。那么还可不可以再优化一下呢? 

(4)欧拉筛(sieve of euler)

       在改进算法中,我们选择只利用k的最小质因子去计算一次k。欧拉筛与埃氏筛法不同的是,对于外层枚举i,无论i是质数,还是是合数,我们都会用i的倍数去筛。但在枚举的时候,我们只枚举i的质数倍
       欧拉筛可以保证每个合数只被枚举到一次,其时间复杂度达到了O(n)
bool numlist[maxn];//标记这个数是不是素数;maxn根据要求的范围确定
vector<int> prime;//vector容器存放求到的素数
void euler()
{
    memset(numlist,true,sizeof(numlist));
    numlist[0]=numlist[1]=false;//注意0和1不是素数哦
    for(int i=2;i<=n;i++)
    {
        if(numlist[i]) prime.push_back(i);//将求得的素数放入容器
        for(int j=0;j<prime.size()&&i*prime[j]<=n;j++)
        {
            numlist[i*prime[j]]=false;//素数的倍数不是素数
            if(i%prime[j]==0) break;//退出循环
        }
    }
}

       问题来了:为什么终止条件是i%prime[j]==0?若i%prime[j]==0,则有i =x * prime[j],则对于prime[j+1]有i * prime[j+1] = prime[j] * x * prime[j+1];因为我们是从小到大枚举素数的,那么又可以说i已经被prime[j]筛过了,所以i乘其他质数的结果也一定是prime[j]的倍数,那么会通过prime[j]筛出,因此不需再筛直接退出。

       通过例题理解一下:洛谷-P3383 【模板】线性筛素数 

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll n,q;
const ll maxn=1e8+7;
bool numlist[maxn];//标记这个数是不是素数;maxn根据要求的范围确定
vector<int> prime;//vector容器存放求到的素数

void euler()
{
    memset(numlist,true,sizeof(numlist));
    numlist[0]=numlist[1]=false;//注意0和1不是素数哦
    for(int i=2;i<=n;i++)
    {
        if(numlist[i]) prime.push_back(i);//将求得的素数放入容器
        for(int j=0;j<prime.size()&&i*prime[j]<=n;j++)
        {
            numlist[i*prime[j]]=false;//素数的倍数不是素数
            if(i%prime[j]==0) break;//退出循环
        }
    }
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>q;
    euler();
    for(int i=1;i<=q;i++)
    {
        ll t;
        cin>>t;
        cout<<prime[t-1]<<endl;
    }
    return 0;
}

       这一题对时间复杂度的要求比较苛刻,所以我采用了欧拉筛(埃氏筛貌似也可)来预处理范围内的所有数,筛出素数存于容器中待用。

2.费马小定理(Fermat's little theorem)

(1)何谓费马小定理?

       费马小定理(Fermat's little theorem)是数论中的一个重要定理,在1636年提出。如果p是一个素数,而整数a不是p的倍数,则有a^(p-1)≡1(mod p)

(2)费马小定理求逆元

       逆元素是指一个可以取消另一给定元素运算的元素。比如说乘法运算中,一个数的逆元是它的倒数。逆元定义为ax ≡ 1 (mod p),再根据费马小定理得  ax ≡ a^(p−1) (mod p)。所以x的逆元为  x ≡ a^(p−2) (mod p)。这样我们直接使用快速幂就可以求出逆元。在mod p条件下,使用逆元做除可以避免取模带来除数的改变。特别注意此方法只适合p为质数的情况下使用。

例题:牛客竞赛-智乃酱的区间乘积

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const ll mod=1e9+7;
ll a[100007],s[100007];
ll n,m;

typedef long long ll;

ll qpow(ll x,ll n)//快速幂
{
    ll ans=1;
    x%=mod;
    while(n)
    {
        if(n&1)
        {
            ans=ans*x%mod;
        }
        x=x*x%mod;
        n>>=1;
    }
    return ans;
}

ll get_inv(ll x)//求x的逆元
{
    return qpow(x,mod-2);
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    s[0]=1;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    for(int i=1;i<=n;i++)
    {
        s[i]=s[i-1]*a[i]%mod;//类似于前缀和
    }
    for(int i=1;i<=m;i++)
    {
        ll l,r;
        cin>>l>>r;
        cout<<s[r]*get_inv(s[l-1])%mod<<endl;//相当于s[r]/s[l-1]
    }
    return 0;
}

       这一题求区间乘积类似于查询区间和,所以我们用到了“前缀和”(下次有机会也讲一讲qwq),当然这里的s[i]其实是前i个元素的乘积。因为前i个数相乘可能会很大很大,所以我们在计算s[i]的时候取了模。问题来了:最后询问的时候需要做除法怎么办?

       利用费马小定理,我们知道在mod p的条件下s[i]^{-1}​是和s[i]^{(p-2)}等价的。所以尽管我们不能直接除s[l],但是可以用乘s[l]的逆元来代替除s[l]没有取模的原始值,也就是前l个数的乘积。

(3)线性逆元的讨论

       求1 ~ n在mod p (n<p,且p为质数)下的所有的乘法逆元时,如果使用(2)中的方法逐一求的话效率较低,所以我们采用了新的求法。(证明方法还等我研究研究awa)
ll inv[maxn];//存储逆元

void solve(int n,ll p)
{
    inv[1]=1;
    for(int i=2;i<=n;i++)
    {
        inv[i]=(p-p/i)*inv[p%i]%p;
    }
}

3.lcm与gcd

       这个定义相信聪明的你一定会

(1)最大公因数(gcd)

       可以用c++自带的库函数 __gcd(a,b),其中a,b不能为浮点数。亦或使用欧几里得算法​​​​​​,时间复杂度为O(log n)

typedef long long ll;

ll gcd(ll a,ll b)
{
    return b==0?a:gcd(b,a%b);
}

gcd的一些性质:

gcd(a, b) = c  ->  gcd(a / c,b / c) = 1

gcd(a * b, c), a ⊥ c  ->  gcd(b, c)    (注意在代数中,“⊥”指互质

gcd(a, b, c) = gcd( gcd(a, b), c)。    gcd的区间可加性,可以应用于线段树或dp等。

(2)最小公倍数(lcm)

typedef long long ll;

ll lcm(ll a,ll b)
{
    return a/gcd(a,b)*b;
}

       注意这里的a先除gcd再乘b,可以防止可能的a * b过大导致溢出。

       例题:牛客竞赛-区间合数的最小公倍数

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=1000000007;

int l,r;
ll ans=1;
vector<int> isprime;


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

void prime()//朴素筛法
{
    for(int i=2;i<=30000;i++)
    {
        if(judge(i)) isprime.push_back(i);
    }
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    prime();
    cin>>l>>r;
    for(int i=0; isprime[i]*2<=r;i++)
    {
        int j;
        for(j=isprime[i];j<=r;j*=isprime[i])
        {
            if(r/j==(l-1)/j) break;

        }
        ans*=j/isprime[i];//求区间质数的最高次幂
        ans%=mod;
    }
    if(ans==1)
    {
        cout<<"-1"<<endl;
    }
    else
    {
        cout<<ans<<endl;
    }
}

       这里就运用到了一个lcm的性质:求区间所有合数的最小公倍数,需要统计区间内每个素数出现的最高的幂。例如,a=2^1*3^5*5^2​,b=2^3*3^2*5^2​,c=2^5*3^3*5^1​,则它们的lcm为 2^5*3^5*5^2​,也就是每个素数的幂取最大值的乘积。

       当然,这里还用到了上面讲到的素数筛法。

4.欧拉降幂

       怎么又是欧拉先引入问题:牛客竞赛-子序列权值乘积

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod=1000000007;

ll n,a[200007],ans=1;

ll qpow(ll x,ll n,ll p=mod)//快速幂
{
    ll ans=1;
    x%=p;
    while(n)
    {
        if(n&1)
        {
            ans=ans*x%p;
        }
        x=x*x%p;
        n>>=1;
    }
    return ans;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
    }
    sort(a,a+n);//升序排序
    for(int i=0;i<n;i++)
    {
        ll sum=qpow(2,i,mod-1)+ qpow(2,n-i-1,mod-1);//欧拉降幂
        ans*=qpow(a[i],sum);//计算,每个数对最终答案的贡献
        ans%=mod;
    }
    cout<<ans<<endl;
}

        我们选择先对序列进行升序排序,而后计算每个数k做序列最小值的次数,也就是2^(k后面的数的个数),同理计算出k做序列最大值的次数,这里是需要用到快速幂的。这个数很大,对吧?然后我们还要累计这个数作为k的幂的乘积,也就是k对答案的贡献。

        在这种幂比较大的情况,例如 a^b​,令b=k(p-1)+c,则有a^b=a^{k(p-1)+c}=({a^{(p-1)}})^k*a^c​。其中由上文中的费马小定理a^{p-1}%p=1​,则可化简为a^b=a^c​ (mod p)。而 c=b%(p-1),也就是说,我们可以通过对幂模p-1来实现降幂化简。

       “艰难方显勇毅,磨砺始得玉成。”大二下学期的课程和实验更多,更难,蓝桥杯,天梯赛,icpc昆明站也要来了。人的一生求上,未必居中;求中,可能居下;求下,必不入流。青春年少,风华正茂,立志必高远,学得雄鹰展翅飞,莫效燕雀安于栖,共勉!  

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

算法学习模板——素数问题、费马小定理、LCM/GCD和欧拉降幂 的相关文章

  • Golang实习蓝湖二面

    蓝湖二面 自我介绍 问题 casbin的策略 ACL RBAB ABAC 他们的区别和应用 JWT的实现 JWT和传统的token有什么区别 三次握手和四次挥手 time wait 为什么需要等待2MSL 什么是完全二叉树 完全二叉树有什么
  • export、import、commit、save、load的区别

    目录 1 docker export 和 docker import2 docker commit3 docker save 和 docker load 1 docker export 和 docker import docker expo
  • docker搭建redis集群模式

    目录 docker 安装redis1 创建redis conf开启redis验证 开启密码 允许redis外地连接后台启动开启redis持久化 2 启动redis容器3 进入容器 redis集群3主3从1 新建6个redis容器2 构建主从
  • SONiC+P4实践——P4Runtime下发ACL表项

    转载请表明出处 注 xff1a SONiC系统为vs版本 Part1 xff1a 实现外部宿主机与SONiC的网络连通 步骤 1 xff1a 打开一个ubuntu系统安装kvm及其依赖 xff08 1 xff09 查看CPU是否支持虚拟化
  • BDD100K自动驾驶数据集格式转YOLO格式

    说明 xff1a 为了用BDD100K数据集训练YOLOV5模型 xff0c 首先需要将BDD100K数据集格式转成YOLOV5支持的输入格式 转换代码如下 xff1a 一 BDD100K转YOLO格式 usr bin env python
  • 全局代理-WINDOWS怎么设置全局代理?

    https blog 51cto com u 15275599 2923545 WINDOWS设置全局代理可以通过以下4个步骤操作来实现 xff1a 1 点击开始菜单 xff0c 然后点击setting xff08 设置 xff09 xff
  • Easyexcle导入导出

    一 导入 1 依赖 lt excel gt lt dependency gt lt groupId gt com alibaba lt groupId gt lt artifactId gt easyexcel lt artifactId
  • Springboot+(linux)redis哨兵模式实现

    下面是主从redis服务 6379主6380从16381从2 下面是多个哨兵 26379哨兵126380哨兵226381哨兵3 windows下redis压缩包 xff08 本文使用的是5 0 13 xff09 Redis xff08 点我
  • 记一次springboot2.1.6配置(mysql)多数据源

    pom xml lt xml version 61 34 1 0 34 encoding 61 34 UTF 8 34 gt lt project xmlns 61 34 http maven apache org POM 4 0 0 34
  • 小车跟随行驶系统(基于MSP-EXP430F5529LP系统板)

    选用材料 xff1a 主控板MSP EXP430F5529LP 陀螺仪 直流减速电机 xff08 可以选用光电编码器 xff0c 霍尔电机不好调节PID xff09 TB6612电机驱动 超声波测距模块 灰度传感器 无线透传 蓝牙模块 xf
  • 使用sheetjs读取excle表格

    span class token comment cdn引入js span span class token operator lt span script lang span class token operator 61 span sp
  • js的六种继承方式

    1 原型链继承 核心 xff1a 将父类的实例作为子类的原型 span class token keyword function span span class token function Parent1 span span class
  • 代理服务器的学习

    一 代理服务器和VPN 1 工作原理 xff08 1 xff09 代理的工作原理是 xff1a 由代理服务器自己去访问你的目标网站 xff0c 并加载它的内容 xff0c 然后再把这些加载过的内容传递到你的窗口上 这样就相当于你在浏览目标网
  • MyBatisPlus(基于starter和Bean方式)

    文章目录 基于boot starter方式基于Bean方式 基于boot starter方式 1 microboot项目 修改配置文件 xff0c 引入所需要的相关依赖库 dependences gradle ext span class
  • web前端开发常用浏览器介绍及运行配置

    1 web前端开发常用浏览器介绍 浏览器是用来检索展示以及传递web信息的应用程序 xff0c 市面上比较常见的浏览器有IE浏览器 火狐浏览器 谷歌浏览器 Safari浏览器和欧朋浏览器等 xff0c 其中IE 火狐和谷歌是目前互联网上的三
  • 数据集格式--图像--目标检测

    一 项目数据集介绍 xff1a 1 COCO数据集 xff1a coco2017 有80个类别 包含交通信号灯和交通标志 红绿灯信号灯没有颜色属性标签 COCO数据集JSON文件格式 xff0c 主要有以下五个键字段 xff1a span
  • elasticsearch排错指南(各种错误~)

    博主在学习es的时候 xff0c 遇到了很多错误 xff0c 这里列举安装时的错误 一 can not run elasticsearch as root 如上图 xff0c 代表不能使用root用户运行es xff0c 这是es的开发团队
  • Ubuntu下的SD卡分区操作(制作Linux启动文件)

    最近在使用SD卡制作Linux启动文件时 xff0c 要进行SD卡的分区操作 总结了主要的流程 xff0c 操作步骤如下 xff1a 1 插入SD卡并挂载到Ubuntu下 xff0c 输入以下命令查看SD卡挂载信息 sudo fdisk l
  • FreeRTOS内核——数据结构链表

    1 数据结构 1 1 list与list item 也就是c语言中的链表与链表结点 单项链表很少用 xff0c 多用双向链表 1 1 1 list List t list就是一串链表 xff0c 具体来说就是这一串链表的头结点 因此 xff
  • github拒绝连接

    设置 xff0c 网络中心 xff0c 双击当前用的网卡 xff0c 点击属性 xff0c 双击ipv4 xff0c DNS改为手动 xff0c 把DNS服务器地址改为1 1 1 1或者8 8 8 8 xff0c 备用的可以不用管 xff0

随机推荐