![同构:编程中的数学](https://wfqqreader-1252317822.image.myqcloud.com/cover/165/48213165/b_48213165.jpg)
1.4 自然数的结构
有了加法和乘法,我们就可以定义更复杂的计算。第一个例子是累加:0+1+2+…
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/18_04.jpg?sign=1738854568-esnndsQx6mv8kBdGXjKkrsRePLPDpOIS-0-6ba072de3c48c140c371d8e87f8607df)
第二个例子是阶乘n!
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/18_05.jpg?sign=1738854568-m6VvgWQHMgXZFp0JP0UxcU3umQd8MoDZ-0-89e015105d2b66b3d8c62e7ddad27aa9)
这两个例子非常相似。智慧生命和智能机器的一大区别就是能否“跳出系统”,到更高的层次进行抽象。这是我们人类心智中最强大、神秘、复杂、难以捉摸的一部分[5]。
我们发现累加和阶乘都有一个针对自然数零的起始值,累加始自零,阶乘始自一。针对递归情况,它们都将某一操作应用到一个自然数和它的后继上。累加是n′+sum(n),阶乘是n′·fact(n)。如果我们把起始值抽象为c,把递归中的操作抽象为h,就可以用一个统一的形式概括累加和阶乘。
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/19_01.jpg?sign=1738854568-dJwIXrkprYnZXFR9AW7LSQZT9vq4Z0qa-0-e4ac7f9b4d236a55d59bdaada357277c)
这是一个在自然数上的递归结构。我们观察一下它在前几个自然数上的表现。
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/19_02.jpg?sign=1738854568-7x4GDwDCcJm0ZLZWTVohkWqyeSTOQuhP-0-bd8586ea1aa0f2e6ecf604b77e0abe90)
其中,hn(c)表示我们叠加在c上重复进行n次h操作。它是原始递归形式的一种[6]。更进一步,如果观察此前我们定义的函数foldn,就会发现它们之间的关系:
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/19_03.jpg?sign=1738854568-zH1amrkQ1wyZR5KBwrBl9BVoaC1dyIwb-0-eb0d13f96786b109b7dcb5a2099a0b0f)
细心的读者会观察到,我们最初定义的foldn带有三个参数,为什么这里只有两个了呢?实际上我们可以写成f(n)=foldn(c,h,n)。当我们仅传递给三元函数foldn前两个参数,它实际上就成为接受一个自然数为参数的一元函数了。我们可以这样看待它:foldn(c,h)(n)。
我们称foldn为自然数上的叠加(fold)操作。令c为zero,h为succ,我们就得到了自然数。如同上面的表格,我们可以得到一个序列:
zero,succ(zero),succ(succ(zero)),…succn(zero),…
如果c不是zero,h不是succ,则foldn(c,h)就描述了和自然数同构[5]的某种事物。我们来看几个例子:
(+m)=foldn(m,succ)
这个例子描述了将自然数n增加m的操作,将它依次作用到自然数上可以产生和自然数同构的序列m,m+1,m+2,…,n+m,…
(·m)=foldn(0,(+m))
这个例子描述了将自然数n乘以m的操作,将它依次作用到自然数上可以产生和自然数同构的序列0,m,2m,3m,…,nm,…
m()=foldn(1,(·m))
这个例子描述了对自然数m取n次幂的操作,将它依次作用到自然数上可以产生和自然数同构的序列1,m,m2,m3,…,mn,…
那么,我们思考出的这个抽象工具foldn能否描述累加和阶乘呢?我们观察下面的这个表格:
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/20_01.jpg?sign=1738854568-YmFCt0jX1KE3xQTYsZpAoNVgxhDZMEp9-0-0a70d6699f08f65aa16db9e7f24b405f)
这里的关键问题是h必须是个二元操作,它能够对n′和f(n)进行运算。为此,我们将c也定义为一个二元数(a,b)[6]。然后针对二元数(a,b)定义某种类似“succ”的操作。最终为了获取结果,我们还需要定义从二元数中抽取a和b的函数:
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/20_02.jpg?sign=1738854568-v48mP07XAU6SUnut6eEO2RF5qXbGyrQm-0-e99c7c144c220a4bdae562518268e272)
这样我们就可以定义累加和阶乘了。首先是累加的定义:
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/20_03.jpg?sign=1738854568-q3fjREUAi3A9An36dvFhWoNdeYuIIl32-0-a216df66e5da076d81b30270d514f2dc)
我们看看,从起始值(0,0)开始,怎样递推出累加的结果。
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/20_04.jpg?sign=1738854568-M3Rp4h38rp5vLrlTjJZd7uu23YEMd6Yb-0-cba2bb320f8279714e68fa79b26dfa5a)
类似地,我们用foldn定义出阶乘。
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/20_05.jpg?sign=1738854568-gWPJAhrdU7wKV7LqSD0TUNFQ9jTJJ82h-0-9fbd117a32c538bd67ffb7b127e717b1)
在累加和阶乘的定义中,我们使用了符号“·”来连接两个函数2nd和foldn(c,h)。我们称之为函数组合,f·g表示先将函数g应用到变量上,然后再将函数f应用到结果上。即(f·g)(x)=f(g(x))。
为了展示这一抽象工具的强大,我们再来看一个例子:斐波那契数列。这一数列是用中世纪数学家斐波那契(见图1.6)命名的。斐波那契来自拉丁文filius Bonacci,意思是波那契之子。斐波那契的父亲当时是商人,在北非以及地中海一带经商,斐波那契逐渐向当地阿拉伯人学习了印度-阿拉伯的数字系统并通过他的著作《算盘书》(Liber Abaci)将其介绍到欧洲。中世纪的欧洲一直使用罗马数字系统,我们今天在一些钟表盘上仍然可以看到罗马数字。例如2018年的罗马数字表示为MMXVIII,其中一个M代表1000,两个M代表2000,X代表10,V表示5,三个I表示3。把这些加起来得到2018。斐波那契引入欧洲的是我们今天仍然使用的位值制十进制系统。它使用了印度数学发明的零,不同的数字在不同的位置上含义不同。这一先进的数字系统极大地方便了计算,广泛应用在记账、利息、汇率等方面。《算盘书》更是对欧洲的数学复兴产生了深远的影响。
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/21_01.jpg?sign=1738854568-Gt1I9v0TXn2gYA7nDfZyxkKa1ETOLs61-0-979c086208d7dbfe9299f0a56ff26ecd)
图1.6 斐波那契(1175—1250)
斐波那契数列来自《算盘书》中的一个问题(见图1.7):兔子在出生两个月后,就有繁殖能力,一对兔子每个月能生出一对小兔子来。如果所有兔子都不死,那么一年以后可以繁殖多少对兔子?开始时有一对兔子。第一个月小兔尚未具备繁殖能力,所以仍然只有一对兔子。第二个月它们生下一对小兔,共有两对。第三个月大兔子又生下一对小兔,而上月生的小兔还在成长,总共有2+1=3对。第四个月有两对大兔子产下两对小兔,加上原有的三对兔子,总共有3+2=5对。按照这样,我们可以得到一个序列:
1,1,2,3,5,8,13,21,34,55,89,144,…
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/21_02.jpg?sign=1738854568-IYGoQGCGtoM3cl4wy7QNqHxtTqOBXp0W-0-91a0710bb051623ecd1f1b308264e308)
图1.7 斐波那契序列展开
这个数列很有规律,从第三项后,任何一项都等于前两项的和。我们可以这样思考它的原理,如果前一个月有m对兔子,这个月有n对兔子,那么增加的一定都是新产下的小兔,共有n-m对,而剩下的都是成年兔子,共m对。到下个月时,这n-m对小兔刚刚成熟,而m对成兔又产下了m对小兔。所以下个月的兔子总数等于小兔加上成兔为(n-m)+m+m=n+m对。根据这一推理,我们可以给出斐波那契数列的递归定义:
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/21_03.jpg?sign=1738854568-QMtcERpFRNLsEIeawymWLnwiEQx9HFjn-0-aaf081812f03800062a37b371eb474f1)
通常将斐波那契数列的起始值定义为0和1[7]。注意到斐波那契数列的起始值是一对自然数,并且递推关系也是一对值。我们可以利用抽象工具foldn给出下面的定义[8]:
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/22_01.jpg?sign=1738854568-ub2cDLhJZj6VGHuMvq63caeY4Ws6IPuR-0-600a236965d6ad91fba3fc6dbdd444f3)
也许读者会好奇,真实的计算机程序能实现这样的定义么?这是不是太理想化了?下面是一段的Haskell语言的程序代码[9],执行fib 10会输出斐波那契数55[10]。
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/22_02.jpg?sign=1738854568-NbhA032nx4YwRpmgcefdpeUmVIfAhX60-0-2b2c45edeb8da23ee9e0427c8b4a88d4)
练习1.2
1.使用foldn定义平方( )2。
2.使用foldn定义( )m,计算给定自然数的m次幂。
3.使用foldn定义奇数的和。它会产生怎样的序列?
4.地面上有一排洞(无限多个),一只狐狸藏在某个洞中。每天狐狸会移动到相邻的下一个洞里。如果每天只能检查一个洞,请给出一个捉到狐狸的策略,并证明这个策略有效(参见图1.8)。如果狐狸每天移动的不止一个洞呢[7]?
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/22_03.jpg?sign=1738854568-p61rgiEwc1aCiCOmE8HQIfFzdJtqxo3M-0-d2632ab88f50522e5ef6587aaa037ad0)
图1.8 《无需语言的证明》封面局部