![同构:编程中的数学](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=1739598244-As3k4YwQBdMBN1ideFDkaPqh9CTwKBis-0-1b5294765f6f2fc88ccd41cd700503af)
第二个例子是阶乘n!
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/18_05.jpg?sign=1739598244-gpQcKRzZikjYI2Ni44CaqebpTJQUZRP8-0-608be918038f057c700eacb80a387a3e)
这两个例子非常相似。智慧生命和智能机器的一大区别就是能否“跳出系统”,到更高的层次进行抽象。这是我们人类心智中最强大、神秘、复杂、难以捉摸的一部分[5]。
我们发现累加和阶乘都有一个针对自然数零的起始值,累加始自零,阶乘始自一。针对递归情况,它们都将某一操作应用到一个自然数和它的后继上。累加是n′+sum(n),阶乘是n′·fact(n)。如果我们把起始值抽象为c,把递归中的操作抽象为h,就可以用一个统一的形式概括累加和阶乘。
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/19_01.jpg?sign=1739598244-lyAnmi9iJ7R27hJwrwrrFhKr9RW2n0O5-0-da5b3a511656b552a32b53dedd935195)
这是一个在自然数上的递归结构。我们观察一下它在前几个自然数上的表现。
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/19_02.jpg?sign=1739598244-PzfFvVKCecxTEaNaaRtXpga5FJ1R8J7X-0-7d513306520a257bf954ba1d5008b580)
其中,hn(c)表示我们叠加在c上重复进行n次h操作。它是原始递归形式的一种[6]。更进一步,如果观察此前我们定义的函数foldn,就会发现它们之间的关系:
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/19_03.jpg?sign=1739598244-kc1pK4ZZpZ6PVthoIuv3Kr8t75AuRNmi-0-d2b8f6d5861182050ac58cc7c9b7090f)
细心的读者会观察到,我们最初定义的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=1739598244-XFEX6Fow02HBlLGGSDWgcxTwnrbNu7at-0-94338ea3f1281a91447c369f1778dc44)
这里的关键问题是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=1739598244-8iszXkPBuAidIbXZEyJZvLsV5F9YVEBE-0-cd43b974e5153bd4003b3e6971e00d27)
这样我们就可以定义累加和阶乘了。首先是累加的定义:
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/20_03.jpg?sign=1739598244-G26zfjSW8PnkVJUo5EfhCn1lszRXc9qo-0-5f30066a17007ad7beaebfda7d74550b)
我们看看,从起始值(0,0)开始,怎样递推出累加的结果。
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/20_04.jpg?sign=1739598244-DNrnQ4cZtCeePOXQDnX9QMMp3EXQkHwL-0-c092142d6142e7b48735d40dbbd467aa)
类似地,我们用foldn定义出阶乘。
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/20_05.jpg?sign=1739598244-0j662tjDEEIn4rt3HO1NzseblMRQWhdt-0-13834f979356c6d1044ea4a85f79188d)
在累加和阶乘的定义中,我们使用了符号“·”来连接两个函数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=1739598244-pOQqQONRT58MSuUuy8URJR7u5yQXc73W-0-997e1671e0620064fbc15ba21c322de7)
图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=1739598244-jhCp6nm3P0NwVw8cifIllCPrkCUInXy8-0-a45e828a2544feea529d8d5ab420958d)
图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=1739598244-CkTSPU7R03h1rCIRF8HQFhc2iK6PIwA9-0-a93c219fb3e009cf587d1752b5a91652)
通常将斐波那契数列的起始值定义为0和1[7]。注意到斐波那契数列的起始值是一对自然数,并且递推关系也是一对值。我们可以利用抽象工具foldn给出下面的定义[8]:
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/22_01.jpg?sign=1739598244-JeaMp6hJr4v7eMslwaF0EfwdGtO73XpM-0-9fd484b488adc0de2be30daba273d127)
也许读者会好奇,真实的计算机程序能实现这样的定义么?这是不是太理想化了?下面是一段的Haskell语言的程序代码[9],执行fib 10会输出斐波那契数55[10]。
![](https://epubservercos.yuewen.com/7F4CCD/27732751902824706/epubprivate/OEBPS/Images/22_02.jpg?sign=1739598244-4XdrLvSFTD9ndZGnBBxVZcZmi8BVksWe-0-700c278ec7ed3d3f60ad68df71ab08fd)
练习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=1739598244-cGu8IedtsdE0811c7Eb9MmlbRonhKlx7-0-812f4d0bfcce8f8bf62884ca376b6cdc)
图1.8 《无需语言的证明》封面局部