修订:@_散沙_Python玩家_

Concrete Math with Python V

Python带你游览具体世界的数学基础

一般来说,高深的数学总会让人有种抗拒心理,但跟具体世界紧密联系的简单数学知识会则不容易让人有如下的疑问:

"我们知道这些知识到底有什么用处?"

这些数学知识第一次出现在高中、大学课本时可能会让人摸不到头脑,而他们往往是基础的计算机科学、统计学、经济学知识的基石。

这部分内容包括:

  • 数列与金融基础
  • 排列组合与概率基础
  • 方程组/矩阵
  • 线性规划
  • 质数与加密
  • 随机数与概率分布

注意:具备扎实的高中数学、理工科本科数学基础的朋友可以简单阅览或者跳过这一部分。

1 质数 Prime

整除:$a*x = b$,$a,x,b$都为整数,称$a整除b$,或者$a|b$

质数(有时也被称为素数):除了1和本身以外,没有其他能够整除自身的数。如$2,3,5,7...$

合数:除了1和本身以外,有其他能够整除自身的数。1不算合数,但4算合数。

小问题:在非负整数的整除关系上,那个数字能被所有数字整除?哪个数字能整数所有数字?


In [1]:
import numpy as np
import scipy.stats as spstat
import pandas as pd
import bokeh
import bokeh.plotting as bplt
import bokeh.palettes as bpal
# 工具配置
TOOLS = "pan,wheel_zoom,box_zoom,box_select"
# 作图配置
plot_config = dict(plot_width=800, plot_height=500, tools=TOOLS)
bplt.output_notebook()


BokehJS successfully loaded.

1.1 获得质数:筛法

筛法,又成为埃拉托色尼筛法,是用来筛选简单筛选质数的。


In [3]:
from IPython.display import Image
Image(url="https://upload.wikimedia.org/wikipedia/commons/b/b9/Sieve_of_Eratosthenes_animation.gif")


Out[3]:

In [2]:
def Sieve(N):
    res = np.int64(np.ones(N))
    # res的第i个元素为1时,表示其为质数,为0时,表示其不为质数
    res[0] = 0
    res[1] = 0
    # 仅需要检查到sqrt(N),因为若N非质数,则其必有一个大于1小于等于sqrt(N)的因子
    upbound = np.int64(np.sqrt(N))
    for i in range(2,upbound):
        if res[i]!=0:
            # i此时未被其因数标记为0,但我们要把从i^2开始,所有i的倍数都标记为0
            res[i**2::i] = 0
        else:
            continue
    return res

In [3]:
for idx,isPrime in enumerate(Sieve(20)):
    if isPrime==1:
        print idx,


2 3 5 7 11 13 17 19

In [4]:
%timeit Sieve(5000)
%timeit Sieve(500000)
%timeit Sieve(50000000)


10000 loops, best of 3: 62.9 µs per loop
100 loops, best of 3: 7.03 ms per loop
1 loop, best of 3: 1.59 s per loop

我们来计算一下筛法的复杂度:若要求出N以内的质数,我们对每一个质数$p$都进行了最多$N/p$次调整

因此$$\sum_{\text{p is prime},p<N} \frac{N}{p} < \sum_{i=2}^N \frac{N}{p} = O(N log N)$$

质数的数量

质数的数量是无穷多个吗?虽然不是那么显而易见,但我们确实有无穷多个质数。

假设质数仅有有限多个(N个)的话,他们是

$$2,3,5,7,... p_N$$

我们考虑一下如下这个整数:$$(\prod_{i=1}^N p_N) +1 $$

任何一个质数都不是它的约束,按照定义,它不仅是一个质数,还比最大的质数$p_N$还要大。

因此我们知道

质数的数量有无穷多个

但如果我们还想知道质数在数轴上存在的密度。

公式指出,$$\pi_N \approx \frac{N}{ln N}$$


In [8]:
fiftymilprime = pd.Series(Sieve(50000000).cumsum())[1:]
primeconverge = fiftymilprime*np.log(fiftymilprime.index)/fiftymilprime.index
indexer = [2**i for i in range(1,25)]
p1 = bplt.figure(title="质数数量与N/ln(N)之比",
       background_fill="#FFFFFF",x_axis_type="log",**plot_config)

p1.line(primeconverge.index[indexer], primeconverge.values[indexer], line_width=3, color='#3288bd', legend='序列收敛到1')

p1.grid.grid_line_alpha=0.3
p1.xaxis.axis_label = 'N'
p1.yaxis.axis_label = '比值'
p1.ygrid.band_fill_color="steelblue"
p1.ygrid.band_fill_alpha = 0.1

bplt.show(p1)


这样当我们再想使用特定数目个质数的时候,我们就知道该在多大的N内寻找这些质数了。

扩展:质数分布的规律

在知道了质数的数量、密度之后,数学家们进一步寻找质数分布的数学规律就是理所应当的了。

(笔者尚未发现孪生素数的工程应用,请读者指正。)

2 质数的用途:研究整除和余数

2.1 整除

用程序验证a是否能够整除b是很容易的,比如验证13能否整除91,或者38能否整除444:


In [11]:
print 91%13 == 0
print 444%38 == 0


True
False

2.2 互质

如果能够同时整除a,b两个整数的正整数只有1,也就是1是a和b的最大公约数(Greatest Common Divisor),那么称a,b互质。

  • 虽然4和9都不是质数,但是能够同时整除它们的正整数只有1。所以它们互质。
  • 虽然7和14中有一个是质数,但是7同时整除7和14。所以它们不互质。

不加证明的给出一个定理:a与b互质,等价于以下条件:

$$(\exists x,y) s.t. ax+by = 1$$

实际上,对于任何$a,b$,

$$(\exists x,y) s.t. ax+by = G.C.D(a,b)$$

欧几里得算法

当然,用程序来计算两个数是否互质有更好的办法:辗转相除法,也称欧几里得算法:

首先用$a$除以$b$得到余数$a'$,$a,b$ 互质等价于 $a',b$ 互质。

这是因为,

$$a = br + a'\\ brx+a'x+by = 1 \\ a'x + b(rx+y) = 1$$

现在我们很确信的知道$a'<b$,那么再用$b$除以$a'$得到余数$b'$,这样循环往复的操作。

如果这两个数反复相除取余数,在某一步其中一个变成了0,那么另外一个非0的数就是$a,b$的最大公约数。

如果这个数恰好等于1的话,那么$a,b$就是互质的。因为最大的公共约数就是1了,也不可能再有别的公共约数了。


In [84]:
def Euclidean(i1,i2):
    a = i1
    b = i2
    while (a*b!=0):
        a = a%b
        ## 交换一下a和b,确保交换后a比b大。
        a,b = b,a
    return a+b

In [85]:
print Euclidean(4,9)
print Euclidean(7,14)


1
7

扩展欧几里得算法

我们刚才已经知道,

$$(\exists x,y) s.t. ax+by = G.C.D(a,b)$$

欧几里得算法能够告诉我们G.C.D(a,b),而扩展欧几里得算法能够给我们$x,y$的信息,十分方便。


In [98]:
def ExtendEuclidean(i1,i2):
    a = i1
    b = i2
    s_2 = 1
    t_2 = 0
    s_1 = 0
    t_1 = 1
    while (a*b!=0):
        q = a/b
        a = a%b
        a,b = b,a
        s_2 = s_2-q*s_1
        t_2 = t_2-q*t_1
        s_2,s_1 = s_1,s_2
        t_2,t_1 = t_1,t_2
    return a+b, s_2,t_2
    
print ExtendEuclidean(4,9)


(1, -2, 1)

第一个1表示$G.C.D(4,9)=1$,而后面的$-2,1$表示$x,y$。

2.3 最大公约数和最小公倍数

刚才提到,欧拉算法能够得到两个数的最大公约数(Greatest Common Divisor, GCD)。

我们把思路变换一下:如果我突然想求两个数的最小公倍数(Least Common Multiple, LCM),应该如何求呢?

我们先用个粗糙的办法:

因为$ab$是$a,b$的公倍数,但可能不是最小的一个公倍数。遍历$i=1,2,...b$,来测试$ai$是否是$b$的倍数即可:


In [25]:
def LCMBrute(i1,i2):
    a = i1
    b = i2
    for i in range(1,i2+1):
        if (a*i)%b==0:
            return a*i

print LCMBrute(4,9)
print LCMBrute(7,14)


36
14

很显然,当$a,b$很大的时候,上述方案很不好。

我们再次不加证明的给出一个定理,使用本次介绍的内容完全可以证明这一公式:

$$(\forall a,b \in I), ab = G.C.D(a,b)* L.C.M(a,b)$$

辗转相除法可以相当快的给出最大公约数,从而可以用如上的公式计算出最小公倍数。

根据这个结果,我们可以轻松的知道,$a,b$互质时,$L.C.M(a,b)=ab$

2.4 孙子定理(Chinese Remainder Theorem)

先简单介绍一个概念:同余

$$p | x_1 - x_2 则 x_1 \equiv x_2 (\text{mod p})$$

同余对于加法、乘法而言都是闭的:

$$x_1 \equiv a (\text{mod p}),x_2 \equiv b (\text{mod p}), \text{ then } $$$$x_1+x_2 \equiv a+b (\text{mod p}) , x_1x_2 \equiv ab (\text{mod p})$$

结合上面的知识我们进一步了解中国古代一个好玩的数学定理。国外常称之为『中国剩余定理』。我们叫孙子定理就可以了。

曾有一个问题:今有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二,问物几何?

而这个问题的解答可以用一首诗概括:

『三人同行七十稀/五树梅花廿一枝/七子团圆正半月/除百零五便得知』

这就暗含着对此类同余问题的一种解法:

$$ x \equiv 2 (\text{mod 3}) \\ x \equiv 3 (\text{mod 5}) \\ x \equiv 2 (\text{mod 7}) $$

那么 $$x \equiv 2*70+3*21+2*15 \equiv 23 (\text{mod 105})$$

因为对于加法、乘法而言,同余性质都能够良好的保持住,仔细观察还可以发现:

  • 70是5,7的公倍数,而且模3余1
  • 21是3,7的公倍数,而且模5余1
  • 15是3,5的公倍数,而且模7余1

这样我们就有: $$ x_1 \equiv 1 (\text{mod 3}) \\ x_1 \equiv 0 (\text{mod 5}) \\ x_1 \equiv 0 (\text{mod 7}), $$

$$ x_2 \equiv 0 (\text{mod 3}) \\ x_2 \equiv 1 (\text{mod 5}) \\ x_2 \equiv 0 (\text{mod 7}), $$$$ x_3 \equiv 0 (\text{mod 3}) \\ x_3 \equiv 0 (\text{mod 5}) \\ x_3 \equiv 1 (\text{mod 7}). $$

的一组解:$x_1=70,x_2=21,x_3=15$,这就是诗句中提示的几个数字。

最后,在这几个数字的辅助下,求得:

$$x \equiv 2*x_1+3*x_2+2*x_3 (\text{mod 105})$$

而这种解法只需要一个条件,那就是同余方程组中的模必须两两互质。

我们可以试验一下,105分解成为357之后,$3,5,7$是两两互质的。我们从0遍历到104:

  • 它们对于mod 105的结果不曾重复
  • 它们分别mod 3, mod 5, mod 7的组合结果也不会重复!多亏了3,5,7两两互质。

In [27]:
for i in range(105):
    print [i,(i%3,i%5,i%7)]


[0, (0, 0, 0)]
[1, (1, 1, 1)]
[2, (2, 2, 2)]
[3, (0, 3, 3)]
[4, (1, 4, 4)]
[5, (2, 0, 5)]
[6, (0, 1, 6)]
[7, (1, 2, 0)]
[8, (2, 3, 1)]
[9, (0, 4, 2)]
[10, (1, 0, 3)]
[11, (2, 1, 4)]
[12, (0, 2, 5)]
[13, (1, 3, 6)]
[14, (2, 4, 0)]
[15, (0, 0, 1)]
[16, (1, 1, 2)]
[17, (2, 2, 3)]
[18, (0, 3, 4)]
[19, (1, 4, 5)]
[20, (2, 0, 6)]
[21, (0, 1, 0)]
[22, (1, 2, 1)]
[23, (2, 3, 2)]
[24, (0, 4, 3)]
[25, (1, 0, 4)]
[26, (2, 1, 5)]
[27, (0, 2, 6)]
[28, (1, 3, 0)]
[29, (2, 4, 1)]
[30, (0, 0, 2)]
[31, (1, 1, 3)]
[32, (2, 2, 4)]
[33, (0, 3, 5)]
[34, (1, 4, 6)]
[35, (2, 0, 0)]
[36, (0, 1, 1)]
[37, (1, 2, 2)]
[38, (2, 3, 3)]
[39, (0, 4, 4)]
[40, (1, 0, 5)]
[41, (2, 1, 6)]
[42, (0, 2, 0)]
[43, (1, 3, 1)]
[44, (2, 4, 2)]
[45, (0, 0, 3)]
[46, (1, 1, 4)]
[47, (2, 2, 5)]
[48, (0, 3, 6)]
[49, (1, 4, 0)]
[50, (2, 0, 1)]
[51, (0, 1, 2)]
[52, (1, 2, 3)]
[53, (2, 3, 4)]
[54, (0, 4, 5)]
[55, (1, 0, 6)]
[56, (2, 1, 0)]
[57, (0, 2, 1)]
[58, (1, 3, 2)]
[59, (2, 4, 3)]
[60, (0, 0, 4)]
[61, (1, 1, 5)]
[62, (2, 2, 6)]
[63, (0, 3, 0)]
[64, (1, 4, 1)]
[65, (2, 0, 2)]
[66, (0, 1, 3)]
[67, (1, 2, 4)]
[68, (2, 3, 5)]
[69, (0, 4, 6)]
[70, (1, 0, 0)]
[71, (2, 1, 1)]
[72, (0, 2, 2)]
[73, (1, 3, 3)]
[74, (2, 4, 4)]
[75, (0, 0, 5)]
[76, (1, 1, 6)]
[77, (2, 2, 0)]
[78, (0, 3, 1)]
[79, (1, 4, 2)]
[80, (2, 0, 3)]
[81, (0, 1, 4)]
[82, (1, 2, 5)]
[83, (2, 3, 6)]
[84, (0, 4, 0)]
[85, (1, 0, 1)]
[86, (2, 1, 2)]
[87, (0, 2, 3)]
[88, (1, 3, 4)]
[89, (2, 4, 5)]
[90, (0, 0, 6)]
[91, (1, 1, 0)]
[92, (2, 2, 1)]
[93, (0, 3, 2)]
[94, (1, 4, 3)]
[95, (2, 0, 4)]
[96, (0, 1, 5)]
[97, (1, 2, 6)]
[98, (2, 3, 0)]
[99, (0, 4, 1)]
[100, (1, 0, 2)]
[101, (2, 1, 3)]
[102, (0, 2, 4)]
[103, (1, 3, 5)]
[104, (2, 4, 6)]

2.5 费马小定理(Fermat Little Theorem)

$$ a^p \equiv a (\text{mod p}) \text{ if p is prime}$$

我们先随便试验一下:


In [32]:
print (2**7-2)%7 == 0
print (34**5-34)%5 == 0
print (21**3-21)%3 == 0


True
True
True

换个写法:$$a (a^{p-1} - 1) = a(a-1)(\sum_{i=0}^{p-2} a^i) = (a-1)(\sum_{i=1}^{p-1} a^i) \equiv 0 \text{ (mod p) }$$

当 $a \equiv 0 \text{ or } 1 \text{ ( mod p ) }$是结果是显然的。

不然,$a,2a,3a,...,(p-1)a$ 除以$p$的余数肯定是$1,2,...,p-1$的重排列(可以证明每一个都不可能同余0,而每一对之间都不可能同余)

通过乘法可得 $$a^{p-1} (p-1)! \equiv (p-1)! \text{ (mod p) } $$

此时,$(p-1)!,p$ 二者互质,因此 $$a^{p-1} \equiv 1 \text{ (mod p) } $$

3 综合应用

结合前面无数的铺垫!我们终于!可以!放!大招!了!

3.1 RSA加密原理:玩转大质数

RSA加密原理由以下关键点组成:

假设$N$是两个质数$p,q$的乘积,同时能够找到$e_1$与$(p-1)(q-1)$互质,也就是能找到

$$e_1 e_2 \equiv 1 \text{ ( mod (p-1)(q-1) )}$$

此时,在仅仅知道$N,e_1$的情况下,我们可以把一个数字A进行一定的转换:

$$B = A^{e_1} \% N$$

只要$A<N$,仅使用$N,e_2$的信息就可以把A给恢复出来(否则,恢复出来的$A'$将是$A$除以$N$的余数):

$$A = B^{e_2} \% N$$

如果你还记得刚才的费马小定理的话:

$$A^{p-1} \equiv 1 \text{ (mod p) } $$$$A^{q-1} \equiv 1 \text{ (mod q) } $$

将上述的两个式子分别乘以$q-1,p-1$次方,套用孙子定理:

$$A^{(p-1)(q-1)} \equiv 1 \text{ ( mod N ) }$$

于是

$$A^{e_1e_2} = A^{1+k(p-1)(q-1)} \equiv A^1 = A \text{ ( mod N ) }$$

我们来实验一下...受限于对大整数的支持,我们只能用小整数玩玩了...


In [151]:
tenmilprimes = pd.Series(Sieve(1000))
primeonly = tenmilprimes[tenmilprimes==1].index
print primeonly[-1],primeonly[-2],primeonly[-3]


997 991 983

In [152]:
p = primeonly[-1]
q = primeonly[-2]
N = p*q
M = (p-1)*(q-1)
e1 = primeonly[-3]
res = ExtendEuclidean(e1,M)
e2 = res[1]
y = res[2]
print e1,e2,(e1*e2)%M


983 54167 1

In [154]:
def expmod(a,e,N):
    eres = e
    modres = 1
    curmod = a
    while eres>0:
        r = eres%2
        eres = eres/2
        modres = (modres*(curmod**r))%N
        curmod = (curmod**2)%N
    return modres

A =  101
B =  expmod(A,e1,N)
A =  expmod(B,e2,N)
print A,B


101 743019

我们把$(N,e_1)$这组信息成为公钥,也就是大家都能用的钥匙。把$A$称为明文,然后通过公钥加密$A$得到的$B$我们称为密文。

把$A$加密成为$B$很容易,但是除非我们知道$N,e_2$的信息,否则从$B$恢复成为$A$是不容易的。

为了知道$e_2$是什么,黑客们就希望从公开信息$N$中分解出$p$和$q$。

而这项工作在$N$足够大(比如$N$来自于两个超过100位大质数相乘)时就变得很困难。

因此,RSA加密算法的安全性来源于大数的质因子分解十分困难这一现象。

3.2 随机数生成器原理:梅森质数

本节内容欲直接了解细节,请参考:

在质数中有一类形式很简洁的质数叫做梅森质数(Mersenne Prime),它有这种性质:

$$p = 2^k - 1 , k\in I$$

人们对它苦苦寻找,因为利用$k$他能够通过某种迭代的方式不断的产生一定长度的数字,而且拥有很多很好的性质:

  • 统计意义上自相关性弱
  • 统计意义上分布均匀
  • 周期较长:序列的模式每$p$次才会重复!

这种性质很适合产生随机数!

举例说明,C语言最常用的用于产生32bit整数的随机数生成器MT19937参数如下:

  • (w, n, m, r) = (32, 624, 397, 31)
  • a = $9908B0DF_{16}$
  • (u, d) = (11, $FFFFFFFF_{16}$)
  • (s, b) = (7, $9D2C5680_{16}$)
  • (t, c) = (15, $EFC60000_{16}$)
  • l = 18

而原本是二进制矩阵与向量的乘法操作、矩阵的加法操作或者向量的截取操作,人们可以通过使用计算机最原始的指令集来完成!(通过异或操作、与操作、或操作、向左向右位移操作完成)。

步骤如下:

  • 通过随机数种子(seed)生成初始x,为长度为n的32bit数值的数组。($wn-r=19937$)
  • 更新$x$:$x_{k+n}$ = $x_{k+m}$ XOR (($x_{k}$ OR $x_{k+1}$)>>1 XOR $a$)
  • 更新$y$:
    • $y$ = $x$ XOR (($x$ >> $u$) AND $d$)
    • $y$ = $y$ XOR (($y$ << $s$) AND $b$)
    • $y$ = $y$ XOR (($y$ << $t$) AND $c$)
  • 在这一阶段输出$z$: $z$ = $y$ XOR ($y$ >> l)

不加修改的给出代码Wikipedia上的代码如下:


In [156]:
def _int32(x):
    # Get the 32 least significant bits.
    return int(0xFFFFFFFF & x)

class MT19937:

    def __init__(self, seed):
        # Initialize the index to 0
        self.index = 624
        self.mt = [0] * 624
        self.mt[0] = seed  # Initialize the initial state to the seed
        for i in range(1, 624):
            self.mt[i] = _int32(
                1812433253 * (self.mt[i - 1] ^ self.mt[i - 1] >> 30) + i)

    def extract_number(self):
        if self.index >= 624:
            self.twist()

        y = self.mt[self.index]

        # Right shift by 11 bits
        y = y ^ y >> 11
        # Shift y left by 7 and take the bitwise and of 2636928640
        y = y ^ y << 7 & 2636928640
        # Shift y left by 15 and take the bitwise and of y and 4022730752
        y = y ^ y << 15 & 4022730752
        # Right shift by 18 bits
        y = y ^ y >> 18

        self.index = self.index + 1

        return _int32(y)

    def twist(self):
        for i in range(624):
            # Get the most significant bit and add it to the less significant
            # bits of the next number
            y = _int32((self.mt[i] & 0x80000000) +
                       (self.mt[(i + 1) % 624] & 0x7fffffff))
            self.mt[i] = self.mt[(i + 397) % 624] ^ y >> 1

            if y % 2 != 0:
                self.mt[i] = self.mt[i] ^ 0x9908b0df
        self.index = 0

In [159]:
myMT = MT19937(1)
for i in xrange(20):
    print myMT.extract_number()


1791095845
4282876139
3093770124
4005303368
491263
550290313
1298508491
4290846341
630311759
1013994432
396591248
1703301249
799981516
1666063943
1484172013
2876537340
1704103302
4018109721
2314200242
3634877716

使用这样的随机数生成器,你就可以很均匀的生成从0到$2^{32}-1$的数字了。如果拿这些数直接除以$2^{32}$,那我们就得到了相当好的从0~1的 均匀分布。关于均匀分布为什么很重要,在下一章中你就知道啦!

同时我们把这种被随机数种子唯一决定的随机数序列称为伪随机数,它的特点如下:

  • 如果我们用它产生用于加密的原始数据,连续extrac_number太长时别人是可以反推出我们的种子以及接下来的序列的。
  • 如果我们用它来做重复性的数学试验,则相同的算法与种子理应产生相同的结果,不管执行程序的机器是否相同!

所以它更适合用于后者的场合~

我们回到主题上来:随机数生成器帮助我们做了无穷无尽的实验,都要归功于$2^{19937}-1$是一个梅森质数,所以人类一直在追寻越来越大的梅森质数,反正闲着也是闲着!

原本用来全球范围内分布式搜索梅森质数的程序GIMPS开发的PRIME95,在十几年间其实是变成了一个测试PC Server稳定与否的程序,这些CPU超频的极限玩家很可能压根不关心什么数学问题,但有意思的是,最近数学家们验证的梅森质数无一例外是借助PRIME95程序找到的。2016年1月,人类发现并验证了截止笔者写作时最大的梅森质数:

$$2^{74207281} - 1$$

这比$2^{19937}-1$可不知道高到哪里去了!

小结

今天,你对奇妙的质数又有了新的认识吗?


In [ ]: