基础数论知识复习笔记

目录

欧拉函数

 快速幂求逆元

拓展欧几里得算法

求组合数

1.dp思想求

2.直接按照公式求

3.卢卡斯定理

例子:卡特兰数

容斥原理

例题:能被整除的数

博弈论

Nim游戏

例题

sg函数

例题


不止会写一些基础的理解,后续会找一些好题,实时更新

欧拉函数

在忘掉了很多学过的东西之后,我开始了漫长的复习之旅;

首先是欧拉函数,要搞清楚欧拉函数,要理解欧拉函数的含义.欧拉函数表示的是在1到n之中和n互质的数的个数.其通式如下:.

即phi[ n ] = n * ( 1 - ( 1 / p1 ) )...i * ( 1 - ( 1 / p1 ) );phi为欧拉函数,p为该数字i的每一个质因数. 欧拉函数性质,欧拉函数后面乘的( 1 - ( 1 / p1 ) )只和欧拉函数的质因数种类有关,和次数无关,我们可以采取试除法来求得n的所有质因数然后直接采用该式子计算.当然也可以采用素数筛的方式来以O(N)的时间复杂度求出1 到 n 每一个数的欧拉函数.

先上板子再理解:

/*
数组p含义为欧拉筛筛出来的素数数组
数组phi是求出来的欧拉函数
*/
void get_oula()
{
    phi[1]=1;
    for(int i=2;i<=n;i++)
    {
        if(vis[i]==0)
        {
            p[++cnt]=i;
            phi[i]=i-1;//1
        }
        for(int j=1;j<=cnt&&i*p[j]<=n;j++)
        {
            vis[i*p[j]]=1;
            if(i%p[j]==0)
            {
                phi[p[j]*i]=p[j]*phi[i];//2
                break;
            } 
            phi[p[j]*i]=(p[j]-1)*phi[i];//3
        }
    }
}

这就是线性筛求欧拉函数的模板.上面加了//标记的就是算欧拉函数的核心,思路如下,线性筛将所有的数分成了三种:

1.vis[i]==0;即该数为素数的情况,因为是素数,此时该数的欧拉函数应该为

phi[i]=i-1

2.vis[i]!=0&&i%p[j]==0,此时的数i*p[j]是一个合数,注意因为i%p[j]==0,所以i是含有这个质因子p[j]的,含有这个质因子,那么该欧拉函数只含有一个( 1 - ( 1 / p[j] ) ).只乘一次,那么可以化为

phi[i*p[j]]=phi[i]*p[j];

3.vis[i]!=0&&i%p[j]!=0,和情况2相反,i和p[j]没有相同质因子所以要多乘一个( 1 - ( 1 / p[j] ) ),则phi[i*p[j]]=phi[i]*phi[j]即为

phi[i]*p[j]*(1-(1/p[j]))=phi[i]*(p[j]-1).

 快速幂求逆元

当存在有a,b,m三个数,b|a(b整除a,即为a%b==0),b,m互质的时候

a/b≡a*x(mod m)       //即为(a/b)==(a*x)%m

可以化简为:

b*x≡1(mod m)

又根据费马小定理:当b^(p-1)≡1(mod p),可化简得到:

b*(b^(p-2))≡1(mod p)

可以得到x=m^(p-2).x是b关于m的乘法逆元.

逆元的三种求法_ccsu_yuyuzi的博客-CSDN博客_可逆元素的逆元怎么求先贴一手我的拓展欧几里得算法详解,理解这个在理解拓展欧几里得之后比较好:https://blog.csdn.net/qq_49593247/article/details/1199740231.费马小定理当存在两个数,a,p.且两者互质,即两者gcd(最大公约数)为1,可以得到a^(p-1)=1(mod p).那么可以化为a*a^(p-2)=1(mod p).那么a的逆元就是a^(p-2).数据比较小可以采用暴力求解,如果数据比较大可以用快速幂求解.2.拓展欧几里得算法如果是拓展欧几https://blog.csdn.net/qq_49593247/article/details/119984441?spm=1001.2014.3001.5502

拓展欧几里得算法

要了解exgcd(拓展欧几里得算法)之前先了解一下gcd,辗转相除法:

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

然后根据裴蜀定理,当存在一对正整数a,b的时候,一定存在正整数x,y令a*x+b*y=gcd(a,b).

可以由此推出exgcd的模板

int exgcd(int a,int b,int &x,int &y)
{
    if(!b)
    {
        x=1,y=0;
        return a;
    }
    int d=exgcd(b,a%b,y,x);
    y-=(a/b*x);
    return d;
}

求组合数

不论是在日常生活中,还是在算法竞赛中,我们都有很多时候都要用到组合数.下面是从acwing的y总那学到的一些在竞赛中可能用得上的组合数求法.

1.dp思想求

组合数可以理解为我们在一个打的集合中有a个不同的物品,从中选b个出来,求有多种选法,从这里入手的话,那么针对于当前的状况,从a物品里面选b个的状态,是可以从上一层的从a-1个物品里面选b个和从a-1个物品中选b-1个,也就是涉及到最后一个增加的物品选与不选的问题,于是很容易的得到递推式子:

则跑两遍for循环就可以求出时间复杂度在o(n^2)内的组合数:

for(int i=0;i<=2000;i++)
        for(int j=0;j<=i;j++)
            if(j==0)
                c[i][j]=1;
            else
                c[i][j]=(c[i-1][j-1]%mod+c[i-1][j]%mod)%mod;

2.直接按照公式求

我们知道组合数的通式:

 那么可以用数组去记录一些数字的阶乘以及其逆元的阶乘的方式,来按照这个公式来求组合数:

void init()
{
    fact[0]=1;
    infact[0]=1;
    for(int i=1;i<=100000;i++)
    {
        fact[i]=fact[i-1]*i%mod;
        infact[i]=infact[i-1]*qmi(i,mod-2)%mod;
    }
}
//记录逆元阶乘和阶乘

printf("%lld\n",fact[a]*infact[a-b]%mod*infact[b]%mod);
//输出结果

3.卢卡斯定理

直接用力卡斯定理(证明我之后有空会再来琢磨):

int c(int a,int b,int p)
{
    if(a<b)
        return 0;
    int res=1;
    for(int i=1,j=a;i<=b;i++,j--)
    {
        res=res*j%p;
        res=res*qmi(i,p-2,p)%p;
    }
    return res;
}
int lucas(int a,int b,int p)
{
    if(a<p&&b<p)
        return c(a,b,p);
    return c(a%p,b%p,p)*lucas(a/p,b/p,p)%p;
}

ps:以上三种求法都是含有取模的,如果不包含取模的话就要涉及高精度了,我只复习了针对竞赛可能会考的


例子:卡特兰数

因为我是在acwing上面复习的,所以最后就遇到了这个题(据说2016年的高考三卷选择题压轴题就是这个):

给定 n 个 0 和 n 个 1,它们将按照某种顺序排成长度为 2n 的序列,求它们能排列成的所有序列中,能够满足任意前缀序列中 0 的个数都不少于 1的个数的序列有多少个。输出的答案对 10^9+7取模。


针对这个题,其实就是要求卡特兰数,也就是:

求法很简单,就用第三种做法求一下,我会先把代码放上来,然后进行对这个题为什么是要求卡特兰数做出证明:

#include<iostream>
#define mod 1000000007
#define int long long
using namespace std;
int fact[200005],infact[200005];
int qmi(int x,int y)
{
    int res=1;
    while(y)
    {
        if(y&1)
            res=res*x%mod;
        y>>=1;
        x=x*x%mod;
    }
    return res;
}
void init()
{
    fact[0]=infact[0]=1;
    for(int i=1;i<=200000;i++)
    {
        fact[i]=fact[i-1]*i%mod;
        infact[i]=infact[i-1]*qmi(i,mod-2)%mod;
    }
    return ;
}
signed main()
{
    int res=0,n;
    init(); 
    scanf("%lld",&n);
    res=fact[n*2]*infact[n]%mod*infact[n]%mod*qmi(n+1,mod-2)%mod;
    printf("%lld",res);
    return 0;
}

证明:

首先,我们可以把这个序列看成一种在二维坐标轴上移动的走法.如果是1就是往上走,如果是0就是往右走.那么题目中的条件:0和1的个数相等就可以转换为我们最后到达的终点横纵坐标是相等的.那么无论n是多大的时候,最后结果都在y=x这条直线上.又因为满足任意前缀序列中 0 的个数都不少于 1的个数,也就是说不论如何,在我们走路的过程中,我们始终不会到达y=x+1这条直线.

 (图很丑,但是没图不好理解,红线为y=x,蓝线为y=x+1)

当我们经过了蓝线时,那就是不合法的情况,此时我们要求所有的不合法情况,就可以以y=x+1为对称轴,将后半段路径进行轴对称变换,会发现他们最后的终点总是(n-1,n+1).所以除去这些情况:

容斥原理

看了一个大佬的博客,我就贴了过来,证明很清晰,链接如下:

容斥原理(翻译) - vici - C++博客

通俗的来讲,容斥定理就是在一个有n个集合的情况下,求的这n个集合的并集的元素个数.计算方法就是将1到n个集合相交的元素个数进行计算(每种情况都不能漏),当选取的集合个数为奇数就加上,偶数就减去.

来点题:


例题:能被整除的数

给定一个整数 n 和 m 个不同的质数 p1,p2,…,pm。请你求出 1∼n,中能被 p1,p2,…,pm 中的至少一个数整除的整数有多少个。

数据范围:1≤m≤16       1≤n,pi≤10^9


该题很明显就是使用容斥定理来进行计算.我们用n/x就可以求出x在1-n中能整除的数的个数.所以核心思路就是求所有题目中所给素数的不同组合方式的乘积结果,用n去做除法就可以得到在题目所给范围中,所以的题目所给质数组合能整除的数.再进行容斥定理的计算即可.

这里求质数组合的方法可以采用二进制枚举,因为m的范围<16,枚举也就2^16,不会超时.枚举1到(1<<m-1),把该二进制数的每一位的0,1状态看成题中所给的m个素数的选与不选的情况.计算即可.

#include<iostream>
#define int long long
using namespace std;
int p[20];
signed main()
{
    int n,m,res=0;
    scanf("%lld%lld",&n,&m);
    for(int i=0;i<m;i++)
        scanf("%lld",&p[i]);
    for(int i=1;i<(1<<m);i++)
//二进制枚举所有情况
    {
        int s=0,t=1;
//t表示该种组合情况下,应该计算被最小公倍数t整除的数的数量
//s表示该中组合情况的大小.来控制容斥定理里面的正负号
        for(int j=0;j<m;j++)
        {
            if(i>>j&1)
            {
                if(t*p[j]>n) 
                {
                    t=-1;
                    break;
                }              
                //该组合超过了题目所给的范围
                s++;
                t*=p[j];
            }
        }
        if(t==-1)
            continue;
        if(s%2==1)
            res+=(n/t);
        else
            res-=(n/t);
    }
    printf("%lld",res);
    return 0;
}

博弈论

Nim游戏

若一个游戏满足:

1.两名玩家交替行动

2.在游戏进行时刻,可以执行的合法行动与轮到哪位玩家无关

3.不能行动的玩家输了

则该游戏为一个公平组合.

首先我们要明确两个概念:

先手必胜态:可以倒一个必败态

先手必败态:无法走到一个必胜态

先上题更好理解吧:


例题

给定 n堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。

问:如果两人都采用最优策略,先手是否必胜。


Nim游戏的胜负其实一开始就已经决定,结论为如果初始所有值的异或结果不为0则先手必胜,反之必败,这里就涉及了一个必胜和必败态的转换,我们分别三种情况讨论:

#include<iostream>
using namespace std;
int main()
{
    int n,x,sum=0;
    scanf("%d",&n);
    for(int i=0;i<n;i++)
    {
        scanf("%d",&x);
        sum^=x;
    }
    if(sum==0)
        printf("No");
    else
        printf("Yes");
    return 0;
}

sg函数

学习sg函数的前提就是了解Nim函数和Mex运算.

Mex运算:定义Mex(x)为求出不属于集合x的最小非负整数,假设x={1,2,3},则Mex(x)=0;而sg函数的性质就是,每个状态的sg值为它能够达到的所有的下一状态的sg值的Mex值,而当状态转移到结尾后,则该点sg值为0.那么这种性质就让人联想到了dfs去求每个点的状态,进一步可以用记忆化来优化.当sg值为0时,是必败态,为什么,因为根据上文描述,此时已经没有可以行动的机会或者转换不到任何一个必败态了.

以上,假设分发生在一个图上的情况(sg函数求的过程就是图的dfs),假设发生在多个图呢?其实就和Nim游戏一样了.我们预处理了所有点的sg函数,多个图的情况就相当于取石子,若sg函数值为0,则无法转换,若不为0,因为Mex函数的性质,它可以转换到所有比它sg值小的情况,好了,这就转换为了Nim游戏了.


例题

给定 n 堆石子以及一个由 k 个不同正整数构成的数字集合 S。现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合 S,最后无法进行操作的人视为失败。问如果两人都采用最优策略,先手是否必胜。


#include<iostream>
#include<cstring>
#include<set>
using namespace std;
int arr[110],sg[10010];
int n,m;
int get_sg(int x)
{   
    if(sg[x]!=-1)
        return sg[x];
    set<int>se;
    for(int i=1;i<=n;i++)
    {
        if(x>=arr[i])
            se.insert(get_sg(x-arr[i]));
    }
    for(int i=0;;i++)
        if(!se.count(i))
            return sg[x]=i;
//此处set的用处就是计算mex值,将之后的mex值记录,取从0开始的没有出现的
}
int main()
{
    int x,res=0;
    memset(sg,-1,sizeof sg);
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&arr[i]);
    scanf("%d",&m);
    for(int i=0;i<m;i++)
    {
        scanf("%d",&x);
        res^=get_sg(x);
    }
    if(res==0)
        printf("No");
    else
        printf("Yes");
    return 0;
}

孙子定理(线性同余)

之后复习会继续写的

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值