模运算

计算机通常以从1970年1月1日(即“Unix era” 的开始)至今所经过的秒数来存储时间,并在所有和时间有关的计算中使用这些时间戳。

而人类则以历史的某一时刻为参照,记录时间,这个时刻通常具有政治或宗教意义。例如,写这段话的时候大约从公元1年(这是六世纪东罗马僧侣对耶稣基督出生日期的最佳估计)至今已经过了大约63882260594秒。

相比计算机要处理数位可能达到11位的整个时间戳,人类情境中需要的时间信息通常更为简单和局限。例如,人们可能只需要知道现在是下午2点,该去吃饭了;或者今天是周四,所以赛百味的每日特价三明治是意大利BMT。人们通常只从整个时间戳中提取出自己所需要的信息,而不需要了解全部内容。这种处理方式(只处理1-2位数字)比处理整个时间戳(11位数字)来得更简便。

问题是:今天是周四,一年后的今天是周几?

首先,我们将一周的每一天赋以一个数值,星期一是0,星期二是1,以此类推,直到星期日是6。在这个系统下,星期四是3。然后,我们添加365(也就是一年的天数)到这个数字上,然后再对7取余数。更方便的算法是,因为365除以7的余数是1,所以一年后的这一天是星期五(3+1=4,对应到我们的系统就是星期五)。然而,如果下一年是闰年,那么会多出一天(366天),所以我们需要在上述计算后再加一天,那么结果就变成了星期六。

余数

定义:如果两个整数aabb的差可以被mm整除,则称aabbmm同余

m(ab)ab(modm)m | (a-b) \Longleftrightarrow a \equiv b(\mod m)

例如,一年中的第42天和第161天是一周中的同一天,因为(16142)=119=17×7(161 - 42) = 119 = 17 \times 7

mm同余是一种等价关系,它将所有整数划分为等价类,称为余数类(residue class)。每一个模mm的余数类可以由其中任何一个成员表示——尽管我们通常使用该类中最小的非负整数(等于所有非负xmodmx \mod m的余数)。

模数算术研究这些余数集,它们对数论至关重要。

**问题:**现在我们一周有mm天,一年有aa天(不考虑闰年)。那么一年、两年、三年等以后的那天是星期几?有多少种可能性?

为了简单起见,假设今天是星期一,所以初始日d0d_0编号为0,然后没过一年,更新如下:

dk+1=(dk+a)modmd_{k+1} = (d_k+a) \mod m

过了kk年后,将变为:

dk=kamodmd_k = k \cdot a \mod m

由于我们一周有mm天,在某个时刻,日期将回到星期一,日期的顺序将开始循环。不同的星期几的数量就是循环的长度,所以我们需要找到使得下式满足的最小的kk

ka0(modm)k \cdot a \equiv 0 (\mod m)

首先,如果a0a \equiv 0,则永远都是星期一。现在我们假设是a≢0a \not ≡ 0这种有意义的情况:

  • 一周7天,m=7m = 7是质数。我们无法找到一个小于mm的数kk使得kak \cdot a能被mm整除。这是因为质数的定义是只能被1和它本身整除的数,不能被分解成两个非零自然数的乘积。因此,如果mm是一个质数,我们将会遍历所有的mm个星期几。
  • 如果mm不为质数,但是aa与它互质(即aamm没有公约数),那么答案仍然是mm,原因类似:aa的除数对帮助乘积对mm整除没有任何帮助。
  • 如果aamm有共同的除数(公约数),那么我们对kak \cdot a进行模mm计算时,只能得到公约数的倍数。举个例子,如果一周有m=10m=10天,一年有a=42a = 42天或者其他偶数天数,则结果将在10以内偶数中循环。如果一年的天数是5的倍数,则结果只会在0和5之间来回切换。

因此,总的来说,答案是mgcd(a,m)\frac{m}{gcd(a,m)},其中gcd(a,m)gcd(a,m)aamm最大公约数

费马定理

现在,我们考虑如果我们用累乘aa代替累加aa,得到这样一串数字

dn=anmodmd_n = a^n \mod m

同样的,由于余数的数量是有限的,所以仍会存在一个循环。但循环的长度是多少呢?事实证明,如果mm是质数,它将包括所有(m1)(m−1)个非零余数。

定理:对任意数aa和一个质数pp,有:

apa(modp)a^p \equiv a (\mod p)

证明

需要注意的是,这只对质数pp成立。我们可以利用这点,来检查给定的数字是否为质数,这比用对数字进行分解的方式速度更快:我们可以随机挑选一个数字aa(一般小于pp),计算apmodpa^p \mod p,检查是否等于aa

这被称为费马素数性检验(Fermat primality test),它是一种概率性检验——只返回“否”或者“可能是”,因为尽管pp为合数,也可能出现apmodpa^p \mod p等于a。在这种情况下,你需要用不同的随机aa来重复测试,直到你对错误正例的概率(false positive probability)感到满意。

质数性检验通常用于生成大质数(大质数在密码学中有重要应用)。前nn个数中有大约nlnn\frac{n}{\ln n}个质数,而这些质数的分布比较均匀。我们可以随机选择一个需要的范围内的数,进行素性检查,如果不是质数就重复操作,直到找到一个质数。这个操作需要进行的次数平均约是O(lnn)O(\ln n)次。

对费马素性检验来说,一个极其糟糕的输入是卡迈克尔数Carmichael numbers)。卡迈克尔数是一个合数nn,但它对所有与其互质的aa都有an11(modn)a^{n-1} ≡ 1(\mod n)。 然而,卡迈克尔数非常罕见,我们在随机选择数字进行素性检验时,碰到卡迈克尔数的概率非常低。

模数除法

在实现常见的算术运算(如加法、减法、乘法等)时,使用余数(取模)是直接且有效的。只需要注意防止整数溢出,并记住在每次运算后都要取模:

c = (a + b) % m;
c = (a - b + m) % m;
c = a * b % m;

对于加法,减法和乘法,我们可以先进行运算然后对结果取模,或者先对每个操作数取模然后进行运算,这两种做法的结果是一样的。但是对于除法来说,这两种做法的结果可能是不同的。例如,82=4\frac{8}{2} = 4,但是

8mod52mod5=324\frac{8 \mod 5}{2 \mod 5} = \frac{3}{2} ≠ 4

在模运算中,我们不能直接进行除法,而是需要找到一个元素,当我们乘以它时,效果等同于除以另一个数字。这个元素称为模乘法逆元(modular multiplicative inverse)。 假设我们要在模pp运算中除以aa,我们需要找到一个数bb,使得(ab)modp=1(a * b) \mod p = 1。也就是说,bb就是aa在模pp下的乘法逆元,因为aa乘以bb后模pp的结果等于1,所以bb就像是1a\frac{1}{a}

当模pp是一个质数时,我们可以使用费马定理来找到这个模乘法逆元。我们将等式两端除以aa两次可以得到:

apaap11ap2a1a^p \equiv a \Longrightarrow a^{p-1} \equiv 1 \Longrightarrow a^{p-2} \equiv a^{-1}

这就意味着,我们只需要计算ap2modpa^{p-2} \mod p,就可以得到我们需要的模乘法逆元。 所以,当我们需要在模pp下除以aa时,我们实际上是将其乘以aa的模pp逆元,也就是计算(bap2)modp(b * a^{p-2}) \mod p。这样,我们可以在模pp下进行除法操作。