DennyQi's Log

01 Algorithms with Numbers

基本算数

一个整数可以用物体的个数来表示。比如用8个点我们就能表示数字8。但这样的话对于大的数字我们就必须用非常多的点,造成了不方便。因此为了更方便地表示一个数,我们通常要选择一个进制。我们在日常生活中常用的是10进制,在计算机中常用的是2进制,等等。

NNaa进制下需要logaN\log_a N位,在bb进制下需要logbN\log_b N,二者的比值logba\log_b^a是常数。因此我们可以认为,对进制的选择是不影响算法的时间复杂度的。

加法

从我们在小学时做的列竖式计算中就可以看出,逐位计算并进位的复杂度是正比于数的位数的。设数的位数为nn,则复杂度是O(n)O(n)。同时,仅仅读入数据就要消耗O(n)O(n),因此这就是加法的最优复杂度了。

乘法

按照列竖式的方法,乘法的复杂度是O(n2)O(n^2)

我们也可以通过这样的递归方法来完成乘法:

xy={2(xy/2) if y is even x+2(xy/2) if y is odd x \cdot y=\left\{\begin{array}{ll} 2(x \cdot\lfloor y / 2\rfloor) & \text { if } y \text { is even } \\ x+2(x \cdot\lfloor y / 2\rfloor) & \text { if } y \text { is odd } \end{array}\right.

递归次数正比于yy的位数nn,每次完成加法的复杂度也是nn,总的复杂度依然是O(n2)O(n^2)

乘法本质上是多项式乘法,通过快速傅里叶变换是可以优化到O(nlogn)O(n \log n)的。我们在讨论“分治”算法时还会来讨论这个问题。

除法

我们想算出xx除以yy的商qq和余数rr。考虑递归,假设已经算出了x/2\lfloor x/2\rfloor除以yy的余数qq'rr'。则有x/2=qy+r\lfloor x/2 \rfloor=q' \cdot y + r'。两边同时乘以2,得到2x/2=2qy+2r2\lfloor x/2 \rfloor=2q' \cdot y + 2r'。如果xx是偶数,那么有x=2qy+2rx=2q' \cdot y + 2r',否则有x=2qy+2r+1x = 2q' \cdot y + 2r'+1。注意如果2r2r'2r+12r'+1超出了yy,要减一个yy。这样就推出了q,rq,r

递归的次数为nn,减yy的复杂度为nn,总复杂度是O(n2)O(n^2)

模运算

对于xx除以NN的商为qq,余数为rr,可以写出x=qN+rx = qN+r,其中rr被限制在{0,,N1}\{0,\cdots,N-1\}内。如果对于yy也有y=qN+ry=q'N+r,那么就称xxyy是同余的。记作

xy(modN)x \equiv y \pmod N

由此可见xyx-y一定是NN的倍数,即xy0(modN)x-y \equiv 0 \pmod N

如果xx(modN)x \equiv x' \pmod N,同时yy(modN)y \equiv y' \pmod N。那么此时有xx=sNx-x' = sN, yy=tNy-y' = tN。那么有x+y=(s+t)N+x+yx+y=(s+t)N+x'+y',所以x+yx+y(modN)x+y \equiv x'+y' \pmod N。而xy=(x+sN)(y+tN)=xy+(tx+sy+stN)Nxy = (x'+sN)(y'+tN) = x'y'+(tx'+sy'+stN)N,所以xyxy(modN)xy \equiv x'y' \pmod N。这告诉我们,在模运算中加法和乘法是可以被同余的项代换的。

模运算中由于每个数都被限制在{0,,N1}\{0,\cdots,N-1\}内,因此加法不会超过2N22N-2,因此加法以后最多只需要减一次NN就能保证模运算的范围。两数相乘不会超过(N1)2(N-1)^2,因此需要做一次除法(区域)才能保证范围,但我们注意到数字的位数最多只会扩展一倍。

注意,在模运算下两个都不为0的数相乘可能得到0。比如5*5在模25意义下为0。

快速幂

在模意义下计算xyx^y要用到快速幂算法。核心是:

xy={(x2)y/2 if y is even x(x2)y/2 if y is odd x^{y}=\left\{\begin{array}{ll} \left(x^{2}\right)^{\lfloor y / 2\rfloor} & \text { if } y \text { is even } \\ x \cdot\left(x^{2}\right)^{\lfloor y / 2\rfloor} & \text { if } y \text { is odd } \end{array}\right.

递归的次数是nn,每次乘法用去n2n^2,复杂度为O(n3)O(n^3).

欧几里得算法(GCD)

对于xyx \geq y,有gcd(x,y)=gcd(xmody,y)gcd(x,y)=gcd(x\mod y,y)。一直这样递归下去,直到xx成为yy的倍数为止,我们就求出了最大公约数。这称为求解最大公约数的辗转相除法。要证明这个关系,只需证明辗转相减法gcd(x,y)=gcd(xy,y)gcd(x,y)=gcd(x-y,y),因为模运算只是减法运算的一种“捷径”。这是自然的,因为x,yx,y都可以看作是由它们的公约数这个基本积木拼成的,它们的差依然是由同样的基本积木组成的。严格地,设x,yx,y有一个公约数为kk,那么有x=skx = s \cdot ky=tky = t \cdot k。于是xy=(st)kx-y = (s-t) \cdot k。因此x,yx,y的任意一个公约数一定也是xy,yx-y,y的一个公约数。所以一定有gcd(x,y)gcd(xy,y)gcd(x,y) \leq gcd(x-y,y)。反之同理,xy,yx-y,y的任意一个公约数一定也是x,yx,y的一个公约数,所以又有gcd(xy,y)gcd(x,y)gcd(x-y,y) \leq gcd(x,y)。联立即得gcd(x,y)=gcd(xy,y)gcd(x,y)=gcd(x-y,y)。这样我们就证明了辗转相除法。

为了分析辗转相除法的复杂度,我们来估计一下xmodyx \mod y的大小。如果yx/2y \leq x/2,那么xmody<yx/2x \mod y < y \leq x/2;如果y>x/2y>x/2,那么xmody=xy<x/2x \mod y = x-y < x/2。因此我们每一次至少把一个数缩小了一半,递归的次数是O(n)O(n)的。而模运算的计算复杂度就是除法的复杂度O(n2)O(n^2),因此总的复杂度是O(n3)O(n^3)

扩展欧几里得算法(EXGCD)

对于方程ax+by=dax+by=d,左边可以提取出a,ba,b的最大公约数,因此dd一定是gcd(a,b)gcd(a,b)的倍数。换句话说,ax+byax+by构成的集合一定是“量子化”的,gcd(a,b)gcd(a,b)是最小的单位。如果dd不是gcd(a,b)gcd(a,b)的倍数,那么方程一定是无解的。

而我们要说明,ax+by=gcd(a,b)ax+by=gcd(a,b)一定是有解的。由欧几里得算法得,gcd(a,b)=gcd(b,amodb)gcd(a,b)=gcd(b,a \mod b)。我们列一个新的方程bx+(amodb)y=gcd(b,amodb)bx'+(a\mod b)y'=gcd(b,a \mod b)。联立可得ax+by=bx+(amodb)yax+by=bx'+(a\mod b)y'。注意到amodb=aba/ba \mod b = a-b\lfloor a/b \rfloor。因此有ax+by=ay+b(xa/by)ax+by=ay'+b(x'-\lfloor a/b \rfloor y')。假设我们已经递归地解得了x,yx',y',就可知x=y,y=xa/byx=y',y=x'-\lfloor a/b\rfloor y'是一组可行的解。而由于gcd(a,b)gcd(a,b)递归的尽头b=0b=0,此时xx一定有解,因此返回去最初的x,yx,y一定有解。这样求解方程的方法就成为扩展欧几里得算法。

我们通过Exgcd已经求出了(x0,y0)(x_0,y_0)ax+by=gcd(a,b)ax+by=gcd(a,b)的一组解,如何用它去找到方程的所有可行解呢?如果a(x0+p)+b(y0+q)=gcd(a,b)a(x_0+p)+b(y_0+q)=gcd(a,b)也是一组解,那么必须满足ap+bq=0ap+bq=0(很像线性代数中的零空间的解)。反过来,所有满足ap+bq=0ap+bq=0p,qp,q一定能够保证(x+p,y+q)(x+p,y+q)还是方程的解。因此我们只需解出ap+bq=0ap+bq=0的所有可行解(p,q)(p,q)即可。设a,ba,b除掉彼此的最大公约数得到a,ba',b'ap+bq=0ap+bq=0当且仅当ap+bq=0a'p+b'q=0。要使得apa'pbb'的倍数,apa'p必须是lcm(a,b)lcm(a',b')的倍数,而gcd(a,b)=1gcd(a',b')=1,所以lcm(a,b)=ablcm(a',b')=a'b'apa'p必须是aba'b'的倍数,即pp必须是bb'的倍数,因此必须满足p=kbgcd(a,b)p=\dfrac{kb}{gcd(a,b)}。而容易验证所有这样的pp都是满足的。综上,ax+by=gcd(a,b)ax+by=gcd(a,b)的全部解就是(x0+kbgcd(a,b),y0kagcd(a,b))(x_0+\dfrac{kb}{gcd(a,b)},y_0-\dfrac{ka}{gcd(a,b)})

对于一般的ax+by=dax+by=d,我们可以解出ax+by=gcd(a,b)ax+by=gcd(a,b),再把得到的解乘上dgcd(a,b)\dfrac{d}{gcd(a,b)}倍,得到一组可行的解(x0,y0)(x_0,y_0)。而后,完全相同的,我们依然利用ap+bq=0ap+bq=0把特解拓展到通解,这个过程中只用到了关于a,ba,b的性质,与等式右侧是无关的,因为我们所作的修正是完全相同的。得到的通解依然是(x0+kbgcd(a,b),y0kagcd(a,b))(x_0+\dfrac{kb}{gcd(a,b)},y_0-\dfrac{ka}{gcd(a,b)})

乘法逆元

我们想要定义模运算下的除法,本质上就是定义模运算下的倒数。如果ax1(modN)ax \equiv 1 \pmod N,就称xxaa在模NN意义下的乘法逆元(其中a,xa,x都在00N1N-1之间)。

并不是所有aa都有乘法逆元的。比如a=0a=0就找不到这样的xx;如果a=2,N=6a=2,N=6,也可以验证没有任何的xx是满足的。由扩展欧几里得,我们发现乘法逆元存在当且仅当gcd(a,N)=1gcd(a,N)=1:如果ax1(modN)ax \equiv 1\pmod N有解,则ax+Ny=1ax+Ny=1有解,这要求gcd(a,N)=1gcd(a,N)=1。而只要gcd(a,N)=1gcd(a,N)=1满足,由EXGCD就可以给出ax+Ny=1ax+Ny=1的一组解。这样得到的解可能会使得xx没有落在00N1N-1内,但根据通解的表达式,xx只能间隔Ngcd(a,N)=N\dfrac{N}{gcd(a,N)}=N地变化。所以我们把xx加上若干个NN使它落在00N1N-1就可以了,这一定是能够做到的。

对于给定的aa,逆元xx是唯一的。因为如果存在xxx' \neq x使得xx'也是aa的乘法逆元,那么就有axax1(modN)ax \equiv ax' \equiv 1 \pmod N。于是a(xx)=tNa(x-x')=tN。由于gcd(a,N)=1gcd(a,N)=1,所以a(xx)a(x-x')一定是a,Na,N的最小公倍数的倍数,即a(xx)=kaNa(x-x')=kaN,于是xx=kNx-x'=kN,这与0x,x<N0 \leq x,x'<N矛盾。

素数

费马小定理

费马小定理指出:如果pp是素数,那么对任意满足1a<p1 \leq a < p的整数aa,一定有ap11(modp)a^{p-1} \equiv 1 \pmod p

我们发现, 对于i=1,,p1i=1,\cdots,p-1aia \cdot i一定是两两不同且不为0的(刚好构成了一个permutation):首先,如果ai0(modp)a \cdot i \equiv 0\pmod p,根据gcd(a,p)=1gcd(a,p)=1aa存在乘法逆元,两边同时乘上aa的乘法逆元,就会得到i0i \equiv 0,矛盾;如果对于iji \neq j成立aiaja \cdot i \equiv a \cdot j,那么同样乘以乘法逆元会得到iji \equiv j,矛盾。因此,aia \cdot i刚好取遍了1,,p11,\cdots,p-1。把他们全都乘起来,就得到ap1!(p1)!(p1)!(modp)a^{p-1}!(p-1)! \equiv (p-1)! \pmod p。而11p1p-1的每个数都没有pp这个质因子,因此(p1)!(p-1)!一定是与pp互质的,因此也有乘法逆元。于是两边同时乘以这个逆元,得到ap11(modp)a^{p-1} \equiv 1 \pmod p。这样就证明了费马小定理。

遗憾的是,费马小定理的逆命题并不成立——我们不能由aN11(modN)a^{N-1} \equiv 1 \pmod N推出NN是素数。反例:合数341=11×31341=11\times 31,却有23401(mod341)2^{340} \equiv 1 \pmod {341}。甚至存在这样的合数NN,使得所有比NN小的与它互质的数aa全都满足aN11(modN)a^{N-1} \equiv 1 \pmod N。(N=561=3×11×17N=561=3\times 11 \times 17)这样的数被称为Carmichael数。

aN11(modN)a^{N-1} \equiv 1 \pmod N虽然无法推出NN是素数,但可以推出gcd(a,N)=1gcd(a,N)=1。如果gcd(a,N)>1gcd(a,N)>1,那么aN1tNa^{N-1}-tN一定是gcd(a,N)gcd(a,N)的倍数,因此永远不可能等于1。

根据费马小定理,我们又得到了另一种求乘法逆元的方式:pp是素数时,aa在模pp下的逆就是ap2modpa^{p-2} \mod p

素数判断

我们可以证明,如果NN不是Carmichael数,即存在aa(与NN互质)使得aN1≢1(modN)a^{N-1} \not\equiv 1 \pmod N,那么这样的aa11N1N-1间至少有一半。我们先任意固定这样的一个aa,它是一个常数,满足N1N-1方不为1。假如我们能找到一个bb满足bN11(modN)b^{N-1} \equiv 1 \pmod{N},那么对于abab(模意义下)有(ab)N1aN1bN1aN1≢1(modN)(ab)^{N-1} \equiv a^{N-1}b^{N-1} \equiv a^{N-1} \not\equiv 1 \pmod {N}。因为aaNN互质,所以有乘法逆元,所以对于某个bb'满足b≢b(modN)b \not\equiv b' \pmod{N},一定成立ab≢ab(modN)ab \not\equiv ab' \pmod N。因此对于每个这样的bb,我们一定可以对应地找到一个abab。前者满足N1N-1次方等于1的条件,后者满足不等于N1N-1次方等于1的条件。由于bN11(modN)b^{N-1} \equiv 1 \pmod{N}成立,bb一定存在乘法逆元,而显然a≢1(modN)a \not \equiv 1 \pmod N,所以b≢ab(modN)b \not\equiv ab \pmod N一定成立。我们一对对选这样的b,abb,ab,会形成两个独立的集合,一个全都是N1N-1次方等于1的,一个全都是N1N-1次方不等于1的。也就意味着,每找到一个等于1的就一定能找到一个不等于1的,所以不等于1的至少占了一半。

所以我们可以设计这样一个素数判定算法,对任意给定的NN,我们在22N1N-1间随机一个aa,计算aN1modNa^{N-1} \mod N。如果答案为1就返回yes否则返回no。如果NN是素数,那么程序一定正确;如果NN不是素数且NN不是Carmichael数,此时返回yes的概率小于1/21/2。也就是说此时程序出错的概率小于1/21/2。如果我们连着做kk次,出错的概率就被降到了1/2k1/2^k

由于Carmichael数是罕见的,我们直接忽略这种情况。这样我们就有了一个高效的算法,复杂度就是计算快速幂的复杂度O(n3)O(n^3)。考虑到kk后的复杂度为O(kn3)O(kn^3)

素数生成

Lagrange指出NN以内的素数个数同阶于NlnN\dfrac{N}{\ln N}。这说明素数是非常密集的。我们可以不停地随机一个NN以内的数,然后用上面的算法判定它是否是一个素数,直到它通过判定为止。这样我们就大概率能得到一个素数了。

每个随机到的数是素数的概率为1lnN\dfrac{1}{\ln N},因此期望lnN\ln N次随机后算法结束,共O(n)O(n)次。每次判定的复杂度是O(n3)O(n^3)。因此总复杂度为O(n4)O(n^4)

密码学

一切发送出去的(通信、电波……)信息都是可能被监听的。想要达成加密的目的就必须有从未发送出去过的“密钥”。如果Alice和Bob都有一个二进制串rr(密钥),那么Alice可以先把要发送的串与rr异或,Bob收到后再与rr异或一次就能得到明文了。但这要求rr很长,而且两人要在不被窃听的情况下交换rr是不容易的。

RSA

RSA是公钥加密的典范。在这里,Bob手上有两个密钥,一个密钥全世界只有他知道,称为私钥;一个密钥向全世界公开,称为公钥。Alice用公钥加密自己的信息,Bob利用私钥解密,这样就永远都不可能有人破解了。

在这里,Bob随机生成两个质数p,qp,q。定义N=pqN=pq,再任意找一个与(p1)(q1)(p-1)(q-1)互质的数ee(这是容易办到的,比如取e=3e=3)。N,eN,e构成了我们的公钥。求出ee在模(p1)(q1)(p-1)(q-1)意义下的乘法逆元dd,把dd作为我们的私钥。

由于ed1(mod(p1)(q1))ed \equiv 1 \pmod{(p-1)(q-1)},可以写出ed=k(p1)(q1)+1ed = k(p-1)(q-1)+1。而xp11(modp)x^{p-1} \equiv 1\pmod p,因此xedxk(p1)(q1)+1x(xp1)k(q1)x(modp)x^{ed}\equiv x^{k(p-1)(q-1)+1} \equiv x \cdot (x^{p-1})^{k(q-1)} \equiv x \pmod p。同理,也有xedx(modq)x^{ed} \equiv x \pmod q。因此xedx=t1p=t2qx^{ed}-x=t_1p=t_2q。说明xedxx^{ed}-xp,qp,q的公倍数,因此有xedx(modpq)x^{ed} \equiv x \pmod{pq},即(xe)dx(modN)(x^{e})^d \equiv x \pmod N。这个式子对于任意的00N1N-1xx都成立,如果把f(x)=xef(x)=x^e看作一个映射,g(x)=xdg(x)=x^d看作一个映射,那么g(f(x))=xg(f(x))=x。这说明g(x)g(x)的象集至少有NN个元素,那么其定义域也至少要有NN个元素。而xx最多只有NN个元素。因此f,gf,g都必须是双射。所以对于任意的xx,我们都可以用ff加密(它依赖于e,Ne,N,因此是公钥加密),接收者都可以用gg来解密(由于涉及到dd,因此是私钥解密)。

RSA的核心在于,作为公钥外人只知道NN而不知道p,qp,q。在NN很大时是难以找出p,qp,q的。因此也就难以找出(p1)(q1)(p-1)(q-1),也就难以求出逆元dd。我们把要发送的信息以xex^e的方式加密,不知道dd就无法解密,只有Bob能够解密。