<前言>
扩展欧几里得
中国剩余定理
快速乘与快速幂
模域组合数
例题及应用
<更新提示>
<第一次更新>继续数论学习,这次再加一些基本算法和模板
<正文>
扩展欧几里得
第二次敲扩欧了,还是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,那么最终的答案就是
具体代码如下:
#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;
}
中国剩余定理
简单的说,中国剩余定理就是求解一个同余方程组
注意,我们在求解时需要一个重要的条件:m 1,m 2,…,m n两两互质,即:
考虑如何求解这个方程组。
首先,记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集合。所有不同组合的个数,叫做组合数。这是组合数的定义,记作
组合数的计算公式如下:
那么,我们在需要计算一个模域下的巨大组合数时,该如何计算呢,这里做简要讲解。
当模数p较小时,我们可以考虑Lucas定理:
递归求解即可。
代码略。
当模数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;
}
具体求组合数代码略。
<后记>
<废话>