因为ACM/ICPC中有些题目是关于数论的,特别是解线性同余方程,所以有必要准备下这方面的知识。关于这部分知识,我先后翻看过很多资料,包括陈景润的《初等数论》、程序设计竞赛例题解、“黑书”和很多网上资料,个人认为讲的最好最透彻的是《算法导论》中的有关章节,看了之后恍然大悟。经过几天的自学,自己觉得基本掌握了其中的“奥妙”。拿出来写成文章。
那么什么是线性同余方程?对于方程:ax≡b(mod m),a,b,m都是整数,求解x 的值。
解题例程:pku1061 青蛙的约会 解题报告
符号说明:
mod表示:取模运算
ax≡b(mod m)表示:(ax - b) mod m = 0,即同余
gcd(a,b)表示:a和b的最大公约数
求解ax≡b(mod n)的原理:
对于方程ax≡b(mod n),存在ax + by = gcd(a,b),x,y是整数。而ax≡b(mod n)的解可以由x,y来堆砌。具体做法,见下面的MLES算法。
第一个问题:求解gcd(a,b)
定理一:gcd(a,b) = gcd(b,a mod b)
实现:古老的欧几里德算法
int Euclid(int a,int b)
{
if(b == 0)
return a;
else
return Euclid(b,mod(a,b));
}
附:取模运算
int mod(int a,int b)
{
if(a >= 0)
return a % b;
else
return a % b + b;
}
第二个问题:求解ax + by = gcd(a,b)
定理二:gcd(b,a mod b) = b * x' + (a mod b) * y'
= b * x' + (a - a / b * b) * y'
= a * y' + b * (x' - a / b * y')
= a * x + b * y
则:x = y'
y = x' - a / b * y'
实现:
triple Extended_Euclid(int a,int b)
{
triple result;
if(b == 0)
{
result.d = a;
result.x = 1;
result.y = 0;
}
else
{
triple ee = Extended_Euclid(b,mod(a,b));
result.d = ee.d;
result.x = ee.y;
result.y = ee.x - (a/b)*ee.y;
}
return result;
}
附:三元组triple的定义
struct triple
{
int d,x,y;
};
第三个问题:求解ax≡b(mod n)
实现:由x,y堆砌方程的解
int MLES(int a,int b,int n)
{
triple ee = Extended_Euclid(a,n);
if(mod(b,ee.d) == 0)
return mod((ee.x * (b / ee.d)),n / ee.d);
else
return -1;
}//返回-1为无解,否则返回的是方程的最小解
说明:ax≡b(mod n)解的个数:
如果ee.d 整除 b 则有ee.d个解;
如果ee.d 不能整除 b 则无解。
第四个问题:求解a≡bi(mod m[i]),用
今天参加了一家做图形软件的公司的笔试,出了这样一道题:一堆鸡蛋,3个3个数余2,5个5个数余1,7个7个数余6,问这堆鸡蛋最少有多少个?
作为数学专业出身的我看到这道题当即笑了,因为这是一个很经典的一次同余方程的问题。当然很快就用曾经记得的算法给出了答案101,但我觉得还不过瘾,又因为觉得这类问题对于搞计算机的实在很重要,所以又回忆了一下初等数论的相关理论知识,特此总结一下!
对于这个问题用数学的语言来描述就是:
N = 2(mod 3) = 1(mod 5) = 6(mod 7);
求解最小N。
由一首诗可以轻易得出求解算法:
“ 三人同行七十稀,五树梅花甘一枝, 七子团圆正半月,除百零五便得知。”
即解为:N = 2*70+1*21+6*15-105*n.(n为使得N不为负的最大正整数,这里就是1了)
那么我们是怎么得到这个公式的呢?
其实早在几千年前中国的一本《孙子算经》就已经提及这个问题的解法了,原文为:
“今有物,不知其数,三三数之,剩二,五五数之,剩三,七七数之,剩二,问物几何?”
答曰:“二十三”
术曰:“三三数之剩二,置一百四十,五五数之剩三,置六十三,七七数之剩二,置三十,并之,得二百三十三,以二百一十减之,即得。凡三三数之剩一,则置七十,五五数之剩一,则置二十一,七七数之剩一,则置十五,即得。”
《孙子算经》的“物不知数”题虽然开创了一次同余式研究的先河,但由于题目比较简单,甚至用试猜的方法也能求得,所以尚没有上升到一套完整的计算程序和理论的高度。真正从完整的计算程序和理论上解决这个问题的,是南宋时期的数学家秦九韶。秦九韶在他的《数书九章》中提出了一个数学方法“大衍求一术”(其实就是后来数学王子高斯提出的解同余方程的方法),系统地论述了一次同余式组解法的基本原理和一般程序。
秦九韶为什么要把他的这一套计算程序和基本原理称为“大衍求一术”呢?这是因为其计算程序的核心问题是要“求一”。所谓“求一”,通俗他说,就是求“一个数的多少倍除以另一个数,所得的余数为一”。那么为什么要“求一”呢?我们可以从“物不知数”题的几个关键数字70、21、15中找到规律。
我们可以看出解此题的关键在于如何确定70、21、15这3个数(105 = 3*5*7)。现简述如下:
归结为一句话就是求3、5、7三个数两两组合相乘的k倍模另外一个数而余1的数 。这样说可能有点抽象,下面具体讲解下大家就会明白了:
首先对于5、7两个数,即为:
70 = 2*5*7 = 1(mod 3)
其中k为2,所以很容易求出第一个70,同样的道理可以求出:
21 = 3*7 = 1(mod 5)
15 = 3*5 = 1(mod 7)
理解了上例解法,我们不难得出著名的中国剩余定理(孙子定理)的一般数学形式了:
一次同余方程组
x = a1(mod m1)
x = a2(mod m2)
...
x = ak(mod mk)
若m1,m2,......,mk这些数两两互质,且定义miMi = m1m2......mk以及CiMi = 1 (mod mi)。则此一次同余方程组的解为:
x = a1M1C1 + a2M2C2 + ......+akMkCk (mod m)。
所以我们可以很容易的把这个面试题推广为:
一堆鸡蛋,3个3个数余a1,5个5个数余a2,7个7个数余a3,问这堆鸡蛋最少有多少个?
甚至推广为:
一堆鸡蛋,n1个n1个数余a1,n2个n2个数余a2,n3个n3个数余 a3 ,....., nm个nm个数余am,问这堆鸡蛋最少有多少个?(其中n1,n2,....., nm互质)
推论1: 方程ax≡b(mod n)对于未知量x有解,当且仅当gcd(a,n) | b (b能被gcd(a,b)整除)。
即:若gcd(a,n)|b ==> 有一个解x,使得ax≡b(mod n).
推论2:方程ax≡b(mod n)或者对模n有d个不同的解(有解当且仅当d | b),其中d=gcd(a,n),或者无解。
定理1:设d=gcd(a,n),假定对整数x'和y'满足d=ax'+by'(比如用扩展Euclid算法求出的一组解)。如果d | b,则该方程对模n有d个不同的解,方程ax≡b(mod n)有一个解x0满足x0=x'*(b/d) mod n 通解为xi = x0 + i*(n / d)(d = 0, 1, 2, ..., d - 1)。特别的设e=x0+n,方程ax≡b(mod n)的最小整数解x1=e mod (n/d),最大整数解x2=x1+(d-1)*(n/d)。
定理2:假设方程ax=b(mod n)有解,且x0是方程的任意一个解,则该方程对模n恰有d个不同的解(d=gcd(a,n)),分别为:xi=x0+i*(n/d) mod n 。
类似地: 可以求ax + by = c的整数解(x, y)。==> ax ≡ c(mod b), d = gcd(a, b) | c 时有解。有一个解x0 = x’* (c / d) mod b, y0 = (-a * x0 - c) / b; 通解xi = x0 + i * (b / d), yi = y0 - i * (a / d) (i = 0, 1, 2, ..., d - 1)。
附:扩展欧几里得代码
int exgcd(int a, int b, int& x, int& y)
{
if(b == 0)
{
x = 1;
y = 0;
return a;
}
int r = exgcd(b, a%b, x, y);
int t = x;
x = y;
y = t - a/b* y;
return r;
}
模板一(解ax = b (mod n) 的最小解)
#include
typedef long long LL;
// return d = gcd(a, b) = ax + by
inline LL extended_euclid(const LL& a, const LL& b, LL &x, LL &y)
{
if (b == 0)
{
x = 1, y = 0;
return a;
}
LL d = extended_euclid(b, a % b, x, y);
LL x1;
x1 = x, x = y, y = x1 - a / b * y;
return d;
}
// return ax = b (mod n) 的最小解
inline LL modular_linear_equation_solver(const LL &a, const LL &b, const LL &n)
{
LL d, x, y;
d = extended_euclid(a, n, x, y); // a 与 n 的最大公约数d
if (b % d)
return -1;
LL x0 = x * (b / d) % n + n; // x0 * d 是 ax = b (mod n) 的一个解
return x0 % (n / d); // ax = b (mod n) 的最小解
}
模板一的例题
for (variable = A; variable != B; variable += C) statement;
3 3 2 16 3 7 2 16 7 3 2 16 3 4 2 16 0 0 0 0
0 2 32766 FOREVER
附上代码:
inline LL extended_euclid(const LL& a, const LL& b, LL &x, LL &y)
{
if (b == 0)
{
x = 1, y = 0;
return a;
}
LL d = extended_euclid(b, a % b, x, y);
LL x1;
x1 = x, x = y, y = x1 - a / b * y;
return d;
}
// return ax = b (mod n) 的最小解
inline LL modular_linear_equation_solver(const LL &a, const LL &b, const LL &n)
{
LL d, x, y;
d = extended_euclid(a, n, x, y); // a 与 n 的最大公约数d
if (b % d)
return -1;
LL x0 = x * (b / d) % n + n; // x0 * d 是 ax = b (mod n) 的一个解
return x0 % (n / d); // ax = b (mod n) 的最小解
}
int main()
{
#ifndef ONLINE_JUDGE
freopen("in.txt", "r", stdin);
#endif
LL A, B, C, k;
while (~scanf("%lld%lld%lld%lld", &A, &B, &C, &k), A || B || C || k)
{
LL d = modular_linear_equation_solver(C, B - A, 1LL << k);
if (d == -1)
puts("FOREVER");
else
printf("%lld\n", d);
}
#ifndef ONLINE_JUDGE
fclose(stdin);
#endif
return 0;
}
模板二(解同余方程组)
int ExGcd( int a, int b, int &x, int &y )
{
if( b == 0 ) { x=1;y=0; return a; }
int r = ExGcd( b, a%b, x, y );
int t = x; x = y; y = t - a/b*y;
return r;
}
int Modline( int r[], int a[], int n )
{
// X = r[i] ( mod a[i] )
int rr = r[0], aa = a[0];
for(int i = 1; i < n; i++ )
{
// aa*x + a[i]*y = ( r[i] - rr );
int C = r[i] - rr, x, y;
int d = ExGcd( aa, a[i], x, y );
if( (C%d) != 0 ) return -1;
int Mod = a[i]/d;
x = ( ( x*(C/d)% Mod ) + Mod ) % Mod;
rr = rr + aa*x; // 余数累加
aa = aa*a[i]/d; // n = n1*n2*...*nk
}
return rr;
}
题目
Time Limit: 1000MS | Memory Limit: 10000K | |
Total Submissions: 128268 | Accepted: 40676 |
Description
Input
Output
Sample Input
0 0 0 0 0 0 0 100 5 20 34 325 4 5 6 7 283 102 23 320 203 301 203 40 -1 -1 -1 -1
Sample Output
Case 1: the next triple peak occurs in 21252 days. Case 2: the next triple peak occurs in 21152 days. Case 3: the next triple peak occurs in 19575 days. Case 4: the next triple peak occurs in 16994 days. Case 5: the next triple peak occurs in 8910 days. Case 6: the next triple peak occurs in 10789 days.
#include
int ExGcd( int a, int b, int &x, int &y )
{
if( b == 0 ) { x=1;y=0; return a; }
int r = ExGcd( b, a%b, x, y );
int t = x; x = y; y = t - a/b*y;
return r;
}
int Modline( int r[], int a[], int n )
{
// X = r[i] ( mod a[i] )
int rr = r[0], aa = a[0];
for(int i = 1; i < n; i++ )
{
// aa*x + a[i]*y = ( r[i] - rr );
int C = r[i] - rr, x, y;
int d = ExGcd( aa, a[i], x, y );
if( (C%d) != 0 ) return -1;
int Mod = a[i]/d;
x = ( ( x*(C/d)% Mod ) + Mod ) % Mod;
rr = rr + aa*x; // 余数累加
aa = aa*a[i]/d; // n = n1*n2*...*nk
}
return rr;
}
// test
int main()
{
int a1, b, c, d, flag = 1;
int r[10], a[10];
while( scanf("%d%d%d%d", &a1, &b, &c,&d))
{
if(a1 == -1 && b == -1 && c == -1 && d == -1)
break;
r[0] = a1;
r[1] = b;
r[2] = c;
a[0] = 23;
a[1] = 28;
a[2] = 33;
int m = Modline( r, a, 3 ) % 21252;
if(m <= d)
printf("Case %d: the next triple peak occurs in %d days.\n", flag++, (m + 21252 - d));
else {
printf("Case %d: the next triple peak occurs in %d days.\n", flag++, m % 21252 - d);
}
}
return 0;
}