给定一个正整数n,如果两个整数a,b的差a-b是n的倍数,则称a,b模n同余。
记为:
a ≡ b (mod n)
直观来说,就是把0,1,…,n-1排成一个环,我们从0开始顺时针数a个数,与从0开始顺时针数b个数,将数到同一个数。(如果a为负数,则从0开始逆时针数-a个数,b同理)
如果a ≡ b (mod n)且c ≡ d (mod n),则容易证明如下性质:
a+c ≡ b+d (mod n)
a-c ≡ b-d (mod n)
ac ≡ bd (mod n)
这与相等关系是类似的,我们注意到高斯消元法解线性方程组就是基于相等关系的这3个性质,所以高斯消元法也是可以解线性同余方程组的。
很多时候我们说a,b模n同余,也理解为n除a和b将有相同的余数,这是很好理解的,既然a,b在环上的位置是相同的,那么a所在的位置的标号(0,1,…,n-1中的一个)当然与b的相同,而这标号正好就是n除a,b的余数。
如果x是n除a的余数,那么显然有a ≡ x (mod n),然而这种情况下我们可以使用一种更强的记号:
x = a mod n (注意这里是“=”而不是“≡”)
这表明x不仅和a同余,而且是和a同余的数中的最小非负整数。
实际上我们上面所说的余数都是指“正的余数”。这是什么意思呢,让我们再澄清一下余数的定义。
自然数的余数的定义如下:
a,d是自然数且d非零,我们可以证明存在唯一的整数q和r,使得a=qd+r,其中0<=r<d。q被称为商,而r被称为余数。
从这个余数的定义我们可以看到,就是说r = a mod d。
我们再来看看整数的余数的定义[2]:
a,d是整数且d非零,则余数r是指满足a=qd+r的整数,其中q是整数且|r|<|d|。
我们注意到这个定义将导致余数的不唯一性。
举个例子,5除8,由于8=1X5+3,所以余数可以是3,又因为8=2X5+(-2),所以余数也可以-2。
在数论中我们通常都取正的那一个作为余数。也就是说n除a的余数在数论中就被认为是与a模n同余的最小非负整数a mod n。
如果n是负数,我们依然可以用符号a mod n表示余数,当然由于n<0,同余的概念已经不能使用了,我们就按照余数的定义取非负的那个。
余数可以方便的计算[1]:
a mod n = a-n*floor(a/n)
floor()表示下取整函数[4]。
这个公式是很好理解的,用a尽量的除n,或者说把a尽可能的分成n份,每份都是一个整数,那么剩余的不能分的就是其余数。
在高级语言中通常提供取模运算,如C/C++中的%算符,Pascal中的mod算符等。
然而这些算符的含义并不完全等同于数论中的mod运算[3],如果不能认识到这一点,可能会导致程序出现意想不到的错误。
余数的定义多种多样(如上就有两种),而计算机表示数的方法更是多种多样[3][5]。所以取模运算的确切结果取决于具体的编程语言甚至是硬件。
但通常来说,n除a的商q与余数r=a mod n都满足如下关系:
q是整数。
a=nq+r。
|r|<|n|。
这就如前文所言,r可能为正或负,这符号的取值依赖于具体的编程语言,例如Pascal和Algol68就规定结果恒为非负的,而C89甚至根本就没有定义这一行为,全依赖于具体编译器的实现,而C99则规定结果与被除数a同号,98年的C++标准也未定义这行为,直到2011年的C++标准中才规定结果与被除数同号[3]。
为了实际编程需要,我们需要了解最常见的编译器,GCC与G++到底是如何定义这些行为的。
我们先从C++编译器(G++)开始,有如下实验程序:
#include <cstdio>
using namespace std;
int main()
{
int a[]={8,8,-8,-8},b[]={5,-5,5,-5,0};
for (int i=0;b[i];++i) printf("%2d mod %2d = %2d\n",a[i],b[i],a[i]%b[i]);
return 0;
}
其运行结果如下:
8 mod 5 = 3
8 mod -5 = 3
-8 mod 5 = -3
-8 mod -5 = -3
可以发现G++的结果是与被除数同号,这与C++2011是一致的。
在来看看C语言(GCC)的情况,有如下实验程序:
#include <stdio.h>
int main()
{
int i,a[]={8,8,-8,-8},b[]={5,-5,5,-5,0};
for (i=0;b[i];++i) printf("%2d mod %2d = %2d\n",a[i],b[i],a[i]%b[i]);
return 0;
}
其运行结果如下:
8 mod 5 = 3
8 mod -5 = 3
-8 mod 5 = -3
-8 mod -5 = -3
可以发现GCC的结果也是与被除数同号,这与C99一致。
可见无论是GCC还是G++,它们的取模运算都与我们数论上的取余数有所区别,所以如果我们想在程序中使用数论上的模运算,应该格外小心。
计算机科学中使用数论模运算通常是为了处理与环状结构有关的问题,例如约瑟夫环问题[6]。这些问题中的除数n通常是正数,所以为了应付这类问题,最保险的做法是编写自己的数论模运算函数:
int mod(int a,int n)
{
int r=a%n;
if (r<0) return r+n;
return r;
}
此函数的正确性是基于如下定理[2]:
a,n是正数且n>0,r[1],r[2]是n除a的两个余数,其中r[1]为正,r[2]为负,则有r[1]=r[2]+n。
这个定理是显然而易于理解的。
参考文献:
[1]Wikipedia:Modular arithmetic.
[2]Wikipedia:Remainder.
[3]Wikipedia:Modulo operation.
[4]Wikipedia:Floor function.
[5]补码为什么是这样定义的.
[6]Wikipedia:Josephus problem.
© 2012, Aikilis. 版权所有. 转载请注明出自http://aikilis.tk/.