引言
形如
为什么取余满足加法,减法和乘法而不满足除法?
为什么求解逆元的多种方法有不同的条件,而且如何认识方法的正确性?
如何理解九余数定理?
…..
通过对同余的了解和对同余定理的学习,我们能够对很多常见的结论有更深的认识。
同时有助于对其他相关算法的学习。
因本蒟蒻水平有限,此博客只作一些简陋的总结。若有错误之处还请指出~
同余的概念
对于同余,常见的下面这个式子:
其数学意义是
因为
故若
假设
则上述同余式可化成:
这也是做题时常使用的一种转化形式。
同余的性质
首先是学过离散的同学都知道的三大基本性质:自反性,对称性,传递性,即:
(1)
(2)若有
(3)若有
然后又是三条用于运算,解释了取模满足加法,减法和乘法的性质:
若有
(4)
(5)
(6)
而为什么除法不满足取模呢,那是因为除法满足的同余性是:
(7)若
则
简单证明瞎扯一下:
由
转化后得出:
两边同除c:
因
故可转化为:
即
另外再补充几个重要性质:
(8)若有
(9)若有
九余数定理 and 弃九法
以上两个数论知识就不详细介绍了,来看这么一道题。
HDU 1163
题意:给你一个正整数n,每次将所有位数加起来得到一个新的数,直到新的数只有一位。输出最后的结果。
思路:我们举
让我们来思考一下到底发生了什么。
把每个数按十进制展开:
每一次操作实质上是将每一位的权值(十位的10,百位的100)变成了1。
如果只是将10变成1,我们可以考虑:
….
但是,我们同时需要将100,1000甚至100000….都变成1,所以,只有对九取模才能达到这样的效果。
如果学过离散数学的同学就能发现,在这里
所以本题只需要求出该数对9取模的数就好了。
另外还有一个细节便是n全是正整数,所以最终答案应该是1~9。但
代码:
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
using namespace std;
typedef long long ll;
const int mod = 9;
int main(){
int n;
while(~scanf("%d",&n) && n){
int ans = 1;
for(int i=1 ;i<=n ;i++) ans = (ans*n)%mod;
if(ans == 0) puts("9");
else printf("%d\n",ans);
}
return 0;
}
由上题我们可以得出一个结论:
一个数对9取余 <==> 该数所有数位的和对9取余。
所以我们又可以解决下面这道题:
HPU 1305
简单思考一下就能轻松过掉啦~
参考代码:
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
using namespace std;
typedef long long ll;
const int mod = 9;
const int A = 1e6 + 10;
char s[A];
int main(){
int q;
while(~scanf("%s%d",s,&q)){
int sum = 0;
int len = strlen(s);
for(int i=0 ;i<len ;i++){
sum = (sum + s[i] - '0')%mod;
}
while(q--){
int pos,val;
scanf("%d%d",&pos,&val);
sum -= s[pos-1] - '0';
sum = ((sum+val)%mod + mod)%mod;
s[pos-1] = val + '0';
if(sum == 0) puts("1");
else puts("0");
}
}
return 0;
}
巧合的是,一个数对
仔细思考我们发现,在十进制下,
那么在任意的
这就是2017年百度之星初赛(A)的一道题:题目链接 HDU 6108
我们可以试着用同余的性质来解决该问题。
假设
我们仍然用
由乘法的同余性可见:
即:
故
对于此题,也就转化成了求约数个数的经典问题。
代码:
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<map>
#include<vector>
#include<algorithm>
using namespace std;
typedef long long ll;
const int A = 1e5 + 10;
bool vis[A];
int pri[A],tot;
void init(){
tot = 0;
for(int i=2 ;i<A ;i++){
if(vis[i] == 0) pri[++tot] = i;
for(int j=1 ;j<=tot && i*pri[j]<A ;j++){
vis[i*pri[j]] = 1;
if(i%pri[j] == 0) break;
}
}
}
int main(){
init();
int T;
scanf("%d",&T);
while(T--){
int n;
scanf("%d",&n);
n--;
ll ans = 1;
for(int i=1 ;i<=tot && pri[i]<=n ;i++){
if(n % pri[i] == 0){
int cnt = 0;
while(n%pri[i] == 0){
n /= pri[i];
cnt++;
}
ans *= (cnt+1);
}
}
if(n>1) ans*=2;
printf("%I64d\n",ans);
}
return 0;
}
乘法逆元
但当所求模不是素数时,可以尝试下面的转换:
下面又是一波看似很有道理的证明:
假设:
则可转化为:
两边同乘B,得:
即:
将B除到等式右边,易知:
证毕。
线性同余方程组
下面介绍中国剩余定理:
形如下面的同余方程组。
若
则该方程组在
其中:
下面是一道例题。
POJ 1006
中国剩余定理裸题
代码:
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
using namespace std;
typedef long long ll;
const int A = 5;
int a[A],m[A];
int M;
void exgcd(int a,int b,int &x,int &y){
if(b == 0){
x = 1;y = 0;
return;
}
exgcd(b,a%b,x,y);
int t = x;
x = y;y = t - (a/b)*y;
}
int CRT(int a[],int m[],int n){
M = 1;
for(int i=1 ;i<=n ;i++) M*=m[i];
int ans = 0;
for(int i=1 ;i<=n ;i++){
int x,y;
int M_i = M / m[i];
exgcd(M_i,m[i],x,y);
x = (x%m[i] + m[i])%m[i];
ans = (ans + a[i]*M_i*x)%M;
}
ans = (ans+M)%M;
return ans;
}
int main(){
int p,e,i,d,_=1;
while(~scanf("%d%d%d%d",&p,&e,&i,&d)){
if(p==-1&&e==-1&&i==-1&&d==-1) break;
a[1] = p,a[2] = e,a[3] = i;
m[1] = 23,m[2] = 28,m[3] = 33;
int ans = ((CRT(a,m,3) - d)%M + M)%M;
if(ans == 0) ans = M;
printf("Case %d: the next triple peak occurs in %d days.\n",_++,ans);
}
return 0;
}
而因
代码:
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
using namespace std;
typedef long long ll;
const int mod = 23*28*33;
int main(){
int a = 5544,b = 14421,c = 1288;
int p,e,i,d,_=1;
while(~scanf("%d%d%d%d",&p,&e,&i,&d)){
if(p==-1&&e==-1&&i==-1&&d==-1) break;
int ans = ((p*a+e*b+c*i-d)%mod + mod)%mod;
if(ans == 0) ans = mod;
printf("Case %d: the next triple peak occurs in %d days.\n",_++,ans);
}
return 0;
}
但当
考虑下面的两个同余方程:
转化后得到:
化简得:
移项得:
由拓展欧几里德,知:
当
否则应用拓展欧几里德解出
解出
故最后可合并为:
下面是两道例题:
POJ 2891
代码:
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
using namespace std;
typedef long long ll;
const int A = 1e3 + 10;
ll a[A],m[A];
ll exgcd(ll a,ll b,ll& x,ll &y){
if(b == 0){
x = 1;y = 0;
return a;
}
ll d = exgcd(b,a%b,x,y);
ll t = x;x = y;y = t-(a/b)*y;
return d;
}
ll CRT(ll a[],ll m[],int n){
ll M = m[1],R = a[1];
for(int i=2 ;i<=n ;i++){
ll x,y;
ll d = exgcd(M,m[i],x,y);
if((a[i]-R) % d) return -1;
x = ((x*(a[i]-R)/d)%(m[i]/d) + (m[i]/d))%(m[i]/d);
R += M*x;
M = M*m[i]/d;
R = (R%M+M)%M;
}
return R;
}
int main(){
int n;
while(~scanf("%d",&n)){
for(int i=1 ;i<=n ;i++) scanf("%I64d%I64d",&m[i],&a[i]);
printf("%I64d\n",CRT(a,m,n));
}
return 0;
}
HDU 1573
模板题
代码:
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<algorithm>
using namespace std;
typedef long long ll;
const int A = 10 + 10;
int a[A],m[A];
int exgcd(int a,int b,int& x,int &y){
if(b == 0){
x = 1;y = 0;
return a;
}
int d = exgcd(b,a%b,x,y);
int t = x;x = y;y = t-(a/b)*y;
return d;
}
int CRT(int a[],int m[],int n,int &M){
int R = a[1];
M = m[1];
for(int i=2 ;i<=n ;i++){
int x,y;
int d = exgcd(M,m[i],x,y);
if((a[i]-R) % d) return -1;
x = ((x*(a[i]-R)/d)%(m[i]/d) + (m[i]/d))%(m[i]/d);
R += M*x;
M = M*m[i]/d;
R = (R%M+M)%M;
}
return R;
}
int main(){
int T;
scanf("%d",&T);
while(T--){
int all,n,M;
scanf("%d%d",&all,&n);
for(int i=1 ;i<=n ;i++) scanf("%d",&m[i]);
for(int i=1 ;i<=n ;i++) scanf("%d",&a[i]);
int ans = CRT(a,m,n,M);
if(ans == -1 || ans>all) puts("0");
else{
all -= ans;
int num = all/M;
if(ans>0) num++; //所求的是正整数个数
printf("%d\n",num);
}
}
return 0;
}
下面是中国剩余定理的另一个重要应用-大数分解和合并
这也是在计算机科学中常见的应用。
假定
由中国剩余定理知:
对于
举个例子:
现有
则
比如我们现在要计算:
我们可以先将两个数表示成他们对
因为
故
同理
当求和时,只需要以下步骤:
因为每一个四元坐标对应唯一一个小于M的整数
故可用中国剩余定理求出:
下面是一道例题:
HDU 4767
此题叫我们求出模
因为n的范围很大,按照常规办法预处理第二类斯特林数求和或者打表一个贝尔三角形在时间和空间复杂度两方面均是爆炸的。
所以我们可以考虑利用Bell数的两个重要的同余性质。(
此时我们可以注意到题目给了一个奇怪的
虽然取模一般套路上都是一个大素数
但此题作为亚洲区域赛的题目, 给了这么一个奇怪的模数,说明切题点大概率就在这个P上。
分解因子后易得
故由我们上面介绍的例子,我们可以分别求出
而每个模数
利用性质1可构建矩阵使用矩阵快速幂
而性质2可将n转化为
此为利用性质2的代码:
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>
using namespace std;
typedef long long ll;
const int P = 95041567;
const int N = 50;
int a[5] = {31,37,41,43,47};
int f[N],s[2][N];
int i,j,x;
ll n;
void init(){
f[0] = f[1] = s[0][0] = 1;
s[0][1]= 2;
for(i=2,x=1;i<N ;i++,x^=1) for(f[i]=s[x][0]=s[x^1][i-1],j=1;j<=i;j++)
s[x][j] = (s[x^1][j-1] + s[x][j-1])%P;
}
ll cal(int x,ll n){
int m = 0,b[N],c[N],d[70];
for(i=0 ;i<=x ;i++) b[i] = f[i]%x;
while(n) d[m++] = n%x,n/=x; //将n转化为x进制
for(i=1 ;i<m ;i++) for(j=1 ;j<=d[i] ;j++){
for(int k=0 ;k<x ;k++) c[k] = (b[k]*i + b[k+1])%x;
c[x] = (c[0]+c[1])%x;
for(int k=0 ;k<=x ;k++) b[k] = c[k];
}
return c[d[0]];
}
ll pow(ll a,ll b,ll p){ll t=1;for(a%=p;b;b>>=1LL,a=a*a%p) if(b&1LL) t=t*a%p;return t;}
ll bell(ll n){
if(n<N) return f[n];
ll t = 0;
for(int i=0;i<5;i++) t = (t + (P/a[i])*pow(P/a[i],a[i]-2,a[i])%P*cal(a[i],n)%P)%P; //CRT合并
return t;
}
int main(){
init();
int T;
scanf("%d",&T);
while(T--){
scanf("%I64d",&n);
printf("%I64d\n",bell(n));
}
return 0;
}
参考资料:
西南大学本科毕业论文
ACdreamer