『基础数论第二篇』

<前言>

扩展欧几里得
中国剩余定理
快速乘与快速幂
模域组合数
例题及应用

<更新提示>

<第一次更新>继续数论学习,这次再加一些基本算法和模板


<正文>

扩展欧几里得

第二次敲扩欧了,还是ax+by=d,先上模板,这次主要讲例题。

inline long long exgcd(long long a,long long b,long long &x,long long &y)
{
    if(b==0){x=1,y=0;return a;}
    long long g=exgcd(b,a%b,y,x);
    y-=a/b*x;
    return g;
    //四行扩欧
}

加深一下对扩欧的理解和回顾:这个算法(函数)就是利用辗转相除法求解不定方程ax+by=d的一组整数解,前提是d为gcd(a,b)的倍数,函数的返回值为gcd(a,b)。

例题:
青蛙的约会

题目描述

两只青蛙在网上相识了,它们聊得很开心,于是觉得很有必要见一面。它们很高兴地发现它们住在同一条纬度线上,于是它们约定各自朝着对方那里跳,直到碰面为止。

可是它们出发之前忘记了一件很重要的事情,既没有问清楚对方的特征,也没有约定见面的具体位置。不过青蛙们都是很乐观的,它们觉得只要一直朝着某个方向跳下去,总能碰到对方的。但是除非这两只青蛙在同一时间跳到同一点上,不然是永远都不可能碰面的。为了帮助这两只乐观的青蛙,你被要求写一个程序来判断这两只青蛙是否能够碰面,会在什么时候碰面。

我们把这两只青蛙分别叫做青蛙A和青蛙B,并且规定纬度线上东经0度处为原点,由东往西为正方向,单位长度1米,这样我们就得到了一条首尾相接的数轴。设青蛙A的出发点坐标是x,青蛙B的出发点坐标是y。青蛙A一次能跳m米,青蛙B一次能跳n米,两只青蛙跳一次所花费的时间相同。纬度线总长L米。现在要你求出它们跳了几次以后才会碰面。
输入格式

一行5个整数x,y,m,n,L,其中x≠y,m、n≠0,L>0。m,n的符号表示了相应的青蛙的前进方向。
输出格式

在单独一行里输出碰面所需要的跳跃次数,如果永远不可能碰面则输出一行“Impossible”。
样例数据

input

1 2 3 4 5

output

4

数据规模与约定

数据有可能超过2^32

zjoi 2002 ,poj 1061,bzoj 1477

时间限制:1s1s

空间限制:256MB

题解
这是一道裸的扩欧模板题,刚接触数论可能看不出来,但一建模就会发现算的就是扩欧方程。推导如下:
假设青蛙走了t次,追了k圈
则可以列出方程:x+mt=y+nt+kl
对方程进行转换:x-y=(m-n)t+kl
这样就得到了ax+by的模型(方程的右边),直接套模板。
由贝祖定理得:有解的充要条件是 d|(x-y),利用gcd(a,b)进行判断即可,如果无解,输出impassible即可。
如果有解,那么我们已经利用exgcd解出一个x_(x0),即为推导时设的t,那么最终的答案就是

x 0 x y d ( m o d l d )

具体代码如下:

#include<bits/stdc++.h>
using namespace std;
#define mset(name,num) memset(name,num,sizeof(name))
long long x,y,n,m,l,x_,y_,ans;
inline long long read()
{
    long long w=0,x=0;char ch;
    while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return w?-x:x;
}
inline long long exgcd(long long a,long long b,long long &x,long long &y)
{
    if(b==0){x=1,y=0;return a;}
    long long g=exgcd(b,a%b,y,x);
    y-=a/b*x;
    return g;
    //四行扩欧
}
int main()
{
    x=read(),y=read();
    n=read(),m=read();
    l=read();
    long long d=exgcd(m-n,l,x_,y_);//求出方程ax+by是否有解,其中a代表青蛙的速度差,b是总长,求解x_,y_,返回值d代表最大公因数 
    if((x-y)%d)cout<<"Impossible"<<endl;//坐标差不整除最大公因数,无解 
    else 
    {
        cout<<((x-y)/d*x_%(l/d)+(l/d))%(l/d)<<endl;//防止负数
    }
    return 0;
}

中国剩余定理

简单的说,中国剩余定理就是求解一个同余方程组

{ x a 1 ( m o d m 1 ) x a 2 ( m o d m 2 ) x a 3 ( m o d m 3 ) . . . x a n ( m o d m n )

注意,我们在求解时需要一个重要的条件:m 1,m 2,…,m n两两互质,即:
g c d ( m 1 , m 2 , . . . , m n ) = 1

考虑如何求解这个方程组。
首先,记M=m1* m2*…*mn,则该方程组在模M意义下有唯一解。
由于m1…n两两互质,令tempi=M/mi,易知tempi与mi也互质。
我们可以用exgcd解方程:x*mi+y*tempi=1,有x*mi+y*tempi≡1(mod mi),因为x*mi取模mi一定为0,所以y*tempi取模mi一定为1,即y*tempi≡1(modmi),两边同乘ai,得y*tempi*ai≡ai(mod mi),这样就得到了一个方程的解,由于temp为仅为求积除以mi,所以方程组的解即为每一个方程的解求和,注意在求和时取模M即可。

模板代码如下:

inline int china(int a[],int m[],int k)//a,m意义如上,k为组数
{
    int x,y,M=1,ans=0;
    for(int i=1;i<=k;i++)M*=m[i];
    for(int i=1;i<=k;i++)
    {
        int temp=M/m[i];
        exgcd(m[i],temp,x,y);
        ans+=(y*m*a[i]);ans%=M;
    }
    if(ans>0)return ans;
    else return ans+M;
} 

例题:
曹冲养猪

题目描述

自从曹冲搞定了大象以后,曹操就开始捉摸让儿子干些事业,于是派他到中原养猪场养猪,可是曹冲满不高兴,于是在工作中马马虎虎,有一次曹操想知道母猪的数量,于是曹冲想狠狠耍曹操一把。

举个例子,假如有16头母猪,如果建了3个猪圈,剩下1头猪就没有地方安家了。如果建造了5个猪圈,但是仍然有1头猪没有地方去,然后如果建造了7个猪圈,还有2头没有地方去。

你作为曹总的私人秘书理所当然要将准确的猪数报给曹总,你该怎么办?
输入格式

第一行包含一个整数n (n <= 10) – 建立猪圈的次数,

解下来n行,每行两个整数ai, bi( bi <= ai <= 1000), 表示建立了ai个猪圈,有bi头猪没有去处。你可以假定ai,aj互质.
输出格式

输出包含一个正整数,即为曹冲至少养母猪的数目。
样例数据

input

3
3 1
5 1
7 2

output

16

数据规模与约定

时间限制:1s1s

空间限制:256MB

题解
裸的中国剩余定理,不再分析,代码如下:
变量名与分析时略有出入,请注意

#include<bits/stdc++.h>
using namespace std;
inline long long read()
{
    long long x=0,w=0;char ch=0;
    while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return w?-x:x;
}
long long n;
struct MOD
{
    long long num,mod;
}m[10080]={};
inline long long exgcd(long long a,long long b,long long &x,long long &y)
{
    if(!b){x=1,y=0;return a;}
    long long g=exgcd(b,a%b,y,x);
    y-=a/b*x;
    return g;
}
inline long long china()
{
    long long sum=1,result=0;
    for(long long i=1;i<=n;i++)sum*=m[i].num;
    for(long long i=1;i<=n;i++)
    {
        long long temp=sum/m[i].num;
        long long x,y;
        exgcd(m[i].num,temp,x,y);
        result=(result+y*temp*m[i].mod);
        result%=sum;
    }
    return (result+sum)%sum;
}
int main()
{
    n=read();
    for(long long i=1;i<=n;i++)m[i].num=read(),m[i].mod=read();
    cout<<china()<<endl;
}

快速乘与快速幂

1.快速幂

对于快速幂,大家并不陌生,即求ab%p,b巨大。
简单的思路就是利用ab=ab/2*ab/2实现log n速率的运算,不断取模p即可,b为奇数时,再乘一个a就行了。
参考代码如下:

inline long long power(long long a,long long b)
{
    long long result=1;
    while(b>0)
    {
        if(b&1)result=(result*a);
        b>>=1;
        a=(a*a);
        a%=mod;
    }
    return result;
}

但是,在某些题目中,如果乘法运算会爆longlong怎么办,如果log n的速度仍然不够怎么办?
我们考虑拓展一种类似于快速幂的乘法运算,快速乘。

2.快速乘

对于a*b,实际相等于a个b相加,我们一样可以用快速幂的做法对它进行优化,边加边取模也能防止爆longlong。
代码如下:

inline long long mul(long long a,long long b)
{
    long long result=0;
    while(b>0)
    {
        if(b&1)result+=a,result%=mod;
        b=b>>1;
        a+=a,a%=mod;
    }
    return result;
}

这样就能在做快速幂时调用快速乘了。

新快速幂代码如下:

inline long long power(long long a,long long b)
{
    long long result=1;
    while(b>0)
    {
        if(b&1)result=mul(result,a);
        b>>=1;
        a=mul(a,a);
    }
    return result;
}

例题:

cont

题目描述

作为史上最贪玩的宏志狗,chty已经数月没交数学作业了,而他在月考中数学考了146分,完爆了全班同学,保平认为自己遭到了光速打脸,于是他给chty出了n道题,教他做人。

chty认为第i道题的难度就是i,他想让这些题目排列起来很漂亮。

chty认为一个漂亮的序列{an}下列两个条件均需满足。

1:a1..ai是单调递减或者单调递增的。

2:ai..an是单调递减或者单调递增的。

他想你告诉他有多少种排列是漂亮的。因为答案很大,所以只需要输出答案模p之后的值。
输入格式

输入数据若干组

每组数据一行两个整数:n,p
输出格式

每组数据输出排列的方案数模p的结果。
样例数据

input

2 233
3 5

output

2
1

数据规模与约定

数据组数<=1000 1<=n,p<=10^18

时间限制:1s1s

空间限制:256MB

题解
这是一道找规律的题,其排列方案应为
n=1 ans=1
n>1 ans=2n-2
2n用快速幂来做,快速幂用快速乘来优化。
代码如下:

#include<bits/stdc++.h>
#define mset(name,num) memset(name,num,sizeof(name))
using namespace std;
long long mod,n;
inline long long read()
{
    long long x=0,w=0;char ch=0;
    while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return w?-x:x;
}
inline long long mul(long long a,long long b)
{
    long long result=0;
    while(b>0)
    {
        if(b&1)result+=a,result%=mod;
        b=b>>1;
        a+=a,a%=mod;
    }
    return result;
}
inline long long power(long long a,long long b)
{
    long long result=1;
    while(b>0)
    {
        if(b&1)result=mul(result,a);
        b>>=1;
        a=mul(a,a);
    }
    return result;
}
int main()
{
    while(cin>>n>>mod)
    {
        if(n==1)cout<<1<<endl;
        else cout<<(power(2,n)-2+mod)%mod<<endl;//避免负数 
    }
    return 0;
}

模域组合数

从n个元素的集合S中,无序选出m个元素,叫做S的一个m集合。所有不同组合的个数,叫做组合数。这是组合数的定义,记作

C n m
,如果两个组合中至少有一个元素不同,就认为他们是不同的集合。
组合数的计算公式如下:
C n m = n ! m ! ( n m ) !

那么,我们在需要计算一个模域下的巨大组合数时,该如何计算呢,这里做简要讲解。
当模数p较小时,我们可以考虑Lucas定理:
C n m C n m o d p m m o d p C n / p m / p ( m o d p )

递归求解即可。
代码略。
当模数p较大时,利用费马小定理或递推法求m!(n-m)!的逆元,再相乘即可。
1费马小定理:
先求出m!(n-m)!%p,再用费马小定理求其逆元再与n!相乘即可。
代码:

//a^(p-2)≡1(mod p)
int quickpower(int a,int b,int c) //a的b次方模c 
{  
    int ans=1;   //记录结果  
    a=a%c;   //预处理,使得a处于c的数据范围之下  
    while(b!=0)  
    {  
        if(b&1) ans=(ans*a)%c;   //如果b的二进制位不是0,那么我们的结果是要参与运算的  
        b>>=1;    //二进制的移位操作,相当于每次除以2,用二进制看,就是我们不断的遍历b的二进制位  
        a=(a*a)%c;   //不断的加倍  
    }  
    return ans;  
}  
long long fermat(long long a,long long p)
{
    return quickpower(a,p-2,p);
}

2递推法
由于m!(n-m)!是阶乘数,所以可以使用递推法求1~m,1~(n-m)的所有逆元,相乘后是等价的。
逆元的递推公式如下:inv[i]=(p-p/i)*inv[p%i]%p。
代码:

inline unsigned long long make_inv(unsigned long long m,unsigned long long p)
{
    memset(inv,0,sizeof(inv));
    inv[1]=1;
    for(int i=2;i<=m;i++)
    inv[i]=(p-p/i)*inv[p%i]%p;
}

具体求组合数代码略。


<后记>


<废话>

猜你喜欢

转载自blog.csdn.net/prasnip_/article/details/80396612
今日推荐