基本算数
一个整数可以用物体的个数来表示。比如用8个点我们就能表示数字8。但这样的话对于大的数字我们就必须用非常多的点,造成了不方便。因此为了更方便地表示一个数,我们通常要选择一个进制。我们在日常生活中常用的是10进制,在计算机中常用的是2进制,等等。
数N在a进制下需要logaN位,在b进制下需要logbN,二者的比值logba是常数。因此我们可以认为,对进制的选择是不影响算法的时间复杂度的。
加法
从我们在小学时做的列竖式计算中就可以看出,逐位计算并进位的复杂度是正比于数的位数的。设数的位数为n,则复杂度是O(n)。同时,仅仅读入数据就要消耗O(n),因此这就是加法的最优复杂度了。
乘法
按照列竖式的方法,乘法的复杂度是O(n2)。
我们也可以通过这样的递归方法来完成乘法:
x⋅y={2(x⋅⌊y/2⌋)x+2(x⋅⌊y/2⌋) if y is even if y is odd
递归次数正比于y的位数n,每次完成加法的复杂度也是n,总的复杂度依然是O(n2)。
乘法本质上是多项式乘法,通过快速傅里叶变换是可以优化到O(nlogn)的。我们在讨论“分治”算法时还会来讨论这个问题。
除法
我们想算出x除以y的商q和余数r。考虑递归,假设已经算出了⌊x/2⌋除以y的余数q′和r′。则有⌊x/2⌋=q′⋅y+r′。两边同时乘以2,得到2⌊x/2⌋=2q′⋅y+2r′。如果x是偶数,那么有x=2q′⋅y+2r′,否则有x=2q′⋅y+2r′+1。注意如果2r′或2r′+1超出了y,要减一个y。这样就推出了q,r。
递归的次数为n,减y的复杂度为n,总复杂度是O(n2)。
模运算
对于x除以N的商为q,余数为r,可以写出x=qN+r,其中r被限制在{0,⋯,N−1}内。如果对于y也有y=q′N+r,那么就称x和y是同余的。记作
x≡y(modN)
由此可见x−y一定是N的倍数,即x−y≡0(modN)。
如果x≡x′(modN),同时y≡y′(modN)。那么此时有x−x′=sN, y−y′=tN。那么有x+y=(s+t)N+x′+y′,所以x+y≡x′+y′(modN)。而xy=(x′+sN)(y′+tN)=x′y′+(tx′+sy′+stN)N,所以xy≡x′y′(modN)。这告诉我们,在模运算中加法和乘法是可以被同余的项代换的。
模运算中由于每个数都被限制在{0,⋯,N−1}内,因此加法不会超过2N−2,因此加法以后最多只需要减一次N就能保证模运算的范围。两数相乘不会超过(N−1)2,因此需要做一次除法(区域)才能保证范围,但我们注意到数字的位数最多只会扩展一倍。
注意,在模运算下两个都不为0的数相乘可能得到0。比如5*5在模25意义下为0。
快速幂
在模意义下计算xy要用到快速幂算法。核心是:
xy={(x2)⌊y/2⌋x⋅(x2)⌊y/2⌋ if y is even if y is odd
递归的次数是n,每次乘法用去n2,复杂度为O(n3).
欧几里得算法(GCD)
对于x≥y,有gcd(x,y)=gcd(xmody,y)。一直这样递归下去,直到x成为y的倍数为止,我们就求出了最大公约数。这称为求解最大公约数的辗转相除法。要证明这个关系,只需证明辗转相减法gcd(x,y)=gcd(x−y,y),因为模运算只是减法运算的一种“捷径”。这是自然的,因为x,y都可以看作是由它们的公约数这个基本积木拼成的,它们的差依然是由同样的基本积木组成的。严格地,设x,y有一个公约数为k,那么有x=s⋅k,y=t⋅k。于是x−y=(s−t)⋅k。因此x,y的任意一个公约数一定也是x−y,y的一个公约数。所以一定有gcd(x,y)≤gcd(x−y,y)。反之同理,x−y,y的任意一个公约数一定也是x,y的一个公约数,所以又有gcd(x−y,y)≤gcd(x,y)。联立即得gcd(x,y)=gcd(x−y,y)。这样我们就证明了辗转相除法。
为了分析辗转相除法的复杂度,我们来估计一下xmody的大小。如果y≤x/2,那么xmody<y≤x/2;如果y>x/2,那么xmody=x−y<x/2。因此我们每一次至少把一个数缩小了一半,递归的次数是O(n)的。而模运算的计算复杂度就是除法的复杂度O(n2),因此总的复杂度是O(n3)。
扩展欧几里得算法(EXGCD)
对于方程ax+by=d,左边可以提取出a,b的最大公约数,因此d一定是gcd(a,b)的倍数。换句话说,ax+by构成的集合一定是“量子化”的,gcd(a,b)是最小的单位。如果d不是gcd(a,b)的倍数,那么方程一定是无解的。
而我们要说明,ax+by=gcd(a,b)一定是有解的。由欧几里得算法得,gcd(a,b)=gcd(b,amodb)。我们列一个新的方程bx′+(amodb)y′=gcd(b,amodb)。联立可得ax+by=bx′+(amodb)y′。注意到amodb=a−b⌊a/b⌋。因此有ax+by=ay′+b(x′−⌊a/b⌋y′)。假设我们已经递归地解得了x′,y′,就可知x=y′,y=x′−⌊a/b⌋y′是一组可行的解。而由于gcd(a,b)递归的尽头b=0,此时x一定有解,因此返回去最初的x,y一定有解。这样求解方程的方法就成为扩展欧几里得算法。
我们通过Exgcd已经求出了(x0,y0)是ax+by=gcd(a,b)的一组解,如何用它去找到方程的所有可行解呢?如果a(x0+p)+b(y0+q)=gcd(a,b)也是一组解,那么必须满足ap+bq=0(很像线性代数中的零空间的解)。反过来,所有满足ap+bq=0的p,q一定能够保证(x+p,y+q)还是方程的解。因此我们只需解出ap+bq=0的所有可行解(p,q)即可。设a,b除掉彼此的最大公约数得到a′,b′,ap+bq=0当且仅当a′p+b′q=0。要使得a′p是b′的倍数,a′p必须是lcm(a′,b′)的倍数,而gcd(a′,b′)=1,所以lcm(a′,b′)=a′b′,a′p必须是a′b′的倍数,即p必须是b′的倍数,因此必须满足p=gcd(a,b)kb。而容易验证所有这样的p都是满足的。综上,ax+by=gcd(a,b)的全部解就是(x0+gcd(a,b)kb,y0−gcd(a,b)ka)。
对于一般的ax+by=d,我们可以解出ax+by=gcd(a,b),再把得到的解乘上gcd(a,b)d倍,得到一组可行的解(x0,y0)。而后,完全相同的,我们依然利用ap+bq=0把特解拓展到通解,这个过程中只用到了关于a,b的性质,与等式右侧是无关的,因为我们所作的修正是完全相同的。得到的通解依然是(x0+gcd(a,b)kb,y0−gcd(a,b)ka)。
乘法逆元
我们想要定义模运算下的除法,本质上就是定义模运算下的倒数。如果ax≡1(modN),就称x是a在模N意义下的乘法逆元(其中a,x都在0到N−1之间)。
并不是所有a都有乘法逆元的。比如a=0就找不到这样的x;如果a=2,N=6,也可以验证没有任何的x是满足的。由扩展欧几里得,我们发现乘法逆元存在当且仅当gcd(a,N)=1:如果ax≡1(modN)有解,则ax+Ny=1有解,这要求gcd(a,N)=1。而只要gcd(a,N)=1满足,由EXGCD就可以给出ax+Ny=1的一组解。这样得到的解可能会使得x没有落在0到N−1内,但根据通解的表达式,x只能间隔gcd(a,N)N=N地变化。所以我们把x加上若干个N使它落在0到N−1就可以了,这一定是能够做到的。
对于给定的a,逆元x是唯一的。因为如果存在x′=x使得x′也是a的乘法逆元,那么就有ax≡ax′≡1(modN)。于是a(x−x′)=tN。由于gcd(a,N)=1,所以a(x−x′)一定是a,N的最小公倍数的倍数,即a(x−x′)=kaN,于是x−x′=kN,这与0≤x,x′<N矛盾。
素数
费马小定理
费马小定理指出:如果p是素数,那么对任意满足1≤a<p的整数a,一定有ap−1≡1(modp)
我们发现, 对于i=1,⋯,p−1,a⋅i一定是两两不同且不为0的(刚好构成了一个permutation):首先,如果a⋅i≡0(modp),根据gcd(a,p)=1,a存在乘法逆元,两边同时乘上a的乘法逆元,就会得到i≡0,矛盾;如果对于i=j成立a⋅i≡a⋅j,那么同样乘以乘法逆元会得到i≡j,矛盾。因此,a⋅i刚好取遍了1,⋯,p−1。把他们全都乘起来,就得到ap−1!(p−1)!≡(p−1)!(modp)。而1到p−1的每个数都没有p这个质因子,因此(p−1)!一定是与p互质的,因此也有乘法逆元。于是两边同时乘以这个逆元,得到ap−1≡1(modp)。这样就证明了费马小定理。
遗憾的是,费马小定理的逆命题并不成立——我们不能由aN−1≡1(modN)推出N是素数。反例:合数341=11×31,却有2340≡1(mod341)。甚至存在这样的合数N,使得所有比N小的与它互质的数a全都满足aN−1≡1(modN)。(N=561=3×11×17)这样的数被称为Carmichael数。
aN−1≡1(modN)虽然无法推出N是素数,但可以推出gcd(a,N)=1。如果gcd(a,N)>1,那么aN−1−tN一定是gcd(a,N)的倍数,因此永远不可能等于1。
根据费马小定理,我们又得到了另一种求乘法逆元的方式:p是素数时,a在模p下的逆就是ap−2modp。
素数判断
我们可以证明,如果N不是Carmichael数,即存在a(与N互质)使得aN−1≡1(modN),那么这样的a在1到N−1间至少有一半。我们先任意固定这样的一个a,它是一个常数,满足N−1方不为1。假如我们能找到一个b满足bN−1≡1(modN),那么对于ab(模意义下)有(ab)N−1≡aN−1bN−1≡aN−1≡1(modN)。因为a与N互质,所以有乘法逆元,所以对于某个b′满足b≡b′(modN),一定成立ab≡ab′(modN)。因此对于每个这样的b,我们一定可以对应地找到一个ab。前者满足N−1次方等于1的条件,后者满足不等于N−1次方等于1的条件。由于bN−1≡1(modN)成立,b一定存在乘法逆元,而显然a≡1(modN),所以b≡ab(modN)一定成立。我们一对对选这样的b,ab,会形成两个独立的集合,一个全都是N−1次方等于1的,一个全都是N−1次方不等于1的。也就意味着,每找到一个等于1的就一定能找到一个不等于1的,所以不等于1的至少占了一半。
所以我们可以设计这样一个素数判定算法,对任意给定的N,我们在2到N−1间随机一个a,计算aN−1modN。如果答案为1就返回yes否则返回no。如果N是素数,那么程序一定正确;如果N不是素数且N不是Carmichael数,此时返回yes的概率小于1/2。也就是说此时程序出错的概率小于1/2。如果我们连着做k次,出错的概率就被降到了1/2k。
由于Carmichael数是罕见的,我们直接忽略这种情况。这样我们就有了一个高效的算法,复杂度就是计算快速幂的复杂度O(n3)。考虑到k后的复杂度为O(kn3)。
素数生成
Lagrange指出N以内的素数个数同阶于lnNN。这说明素数是非常密集的。我们可以不停地随机一个N以内的数,然后用上面的算法判定它是否是一个素数,直到它通过判定为止。这样我们就大概率能得到一个素数了。
每个随机到的数是素数的概率为lnN1,因此期望lnN次随机后算法结束,共O(n)次。每次判定的复杂度是O(n3)。因此总复杂度为O(n4)。
密码学
一切发送出去的(通信、电波……)信息都是可能被监听的。想要达成加密的目的就必须有从未发送出去过的“密钥”。如果Alice和Bob都有一个二进制串r(密钥),那么Alice可以先把要发送的串与r异或,Bob收到后再与r异或一次就能得到明文了。但这要求r很长,而且两人要在不被窃听的情况下交换r是不容易的。
RSA
RSA是公钥加密的典范。在这里,Bob手上有两个密钥,一个密钥全世界只有他知道,称为私钥;一个密钥向全世界公开,称为公钥。Alice用公钥加密自己的信息,Bob利用私钥解密,这样就永远都不可能有人破解了。
在这里,Bob随机生成两个质数p,q。定义N=pq,再任意找一个与(p−1)(q−1)互质的数e(这是容易办到的,比如取e=3)。N,e构成了我们的公钥。求出e在模(p−1)(q−1)意义下的乘法逆元d,把d作为我们的私钥。
由于ed≡1(mod(p−1)(q−1)),可以写出ed=k(p−1)(q−1)+1。而xp−1≡1(modp),因此xed≡xk(p−1)(q−1)+1≡x⋅(xp−1)k(q−1)≡x(modp)。同理,也有xed≡x(modq)。因此xed−x=t1p=t2q。说明xed−x是p,q的公倍数,因此有xed≡x(modpq),即(xe)d≡x(modN)。这个式子对于任意的0到N−1的x都成立,如果把f(x)=xe看作一个映射,g(x)=xd看作一个映射,那么g(f(x))=x。这说明g(x)的象集至少有N个元素,那么其定义域也至少要有N个元素。而x最多只有N个元素。因此f,g都必须是双射。所以对于任意的x,我们都可以用f加密(它依赖于e,N,因此是公钥加密),接收者都可以用g来解密(由于涉及到d,因此是私钥解密)。
RSA的核心在于,作为公钥外人只知道N而不知道p,q。在N很大时是难以找出p,q的。因此也就难以找出(p−1)(q−1),也就难以求出逆元d。我们把要发送的信息以xe的方式加密,不知道d就无法解密,只有Bob能够解密。