提示:本文内容来源于UCB CS61A课程,详情请点击CS 61A: Structure and Interpretation of Computer Programs
文章目录
前言
本文为2020 UCB CS61a 课程 Homework03 内容
提示:以下是本篇文章正文内容,叙述部分参考作业描述,由本人与chatgpt共同翻译,思路部分及作业部分为作者完成,仅供参考,若有错误欢迎评论指正
Homework 3: 高阶函数、自引用、递归、树形递归
Q1: Compose
要求实现一个高阶函数composer
- 该高阶函数的返回值为函数
func
和函数func_adder
,其中func
是一个单参数函数,该函数复合了目前为止传入的所有参数函数(最后复合的最先调用,即在最内层,参见doctest
和样例)func_adder
是一个复合器,便于在func
上复合更多的函数,即当func_adder
被调用时(传入参数g),该函数将g
与func
复合,并返回一个新的func
和func_adder
- 如果
composer
没有被传入任何参数,那么返回它identity
函数
样例如下:
>>> add_one = lambda x: x + 1
>>> square = lambda x: x * x
>>> times_two = lambda x: x + x
>>> f1, func_adder = composer()
>>> f1(1)
1
>>> f2, func_adder = func_adder(add_one)
>>> f2(1)
2 # 1 + 1
>>> f3, func_adder = func_adder(square)
>>> f3(3)
10 # 1 + (3**2)
>>> f4, func_adder = func_adder(times_two)
>>> f4(3)
37 # 1 + ((2 * 3) **2)
课程组给了一个代码框架,这个框架本身用来完成第一个样例是没有问题的
$ python3
Python 3.10.6 (main, Mar 10 2023, 10:55:28) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> def composer(func=lambda x: x):
... """
... Returns two functions -
... one holding the composed function so far, and another
... that can create further composed problems.
... """
... def func_adder(g):
... "*** YOUR CODE HERE ***"
... return func, func_adder
...
>>> f1, func_adder = composer()
>>> f1(1)
1
观察样例2
>>> f2, func_adder = func_adder(add_one) # add_one -> g
>>> f2(1)
2
样例2要求返回两个函数,前一个为identity(add_one(x))
,也可以记作add_one
,后一个等价于lambda g: identity(add_one(<下一步要复合的函数>))
后复合的先调用,因此对fun_adder
的定义如下:
def func_adder(g):
"*** YOUR CODE HERE ***"
return composer(lambda x: func(g(x)))
其中func
指的是上一步已经完成复合的函数,g是指下一步要复合的函数,而func(g(
又会在传入conposer
成为新的func
注:这一步中python解释器在查看func时发现当前环境中没有func
,因此会前往其父环境中查找
完整代码如下:
def composer(func=lambda x: x):
"""
Returns two functions -
one holding the composed function so far, and another
that can create further composed problems.
"""
def func_adder(g):
"*** YOUR CODE HERE ***"
return composer(lambda x: func(g(x)))
return func, func_adder
高阶函数用起来挺别扭的感觉,有疑惑可以到pythontutor查看,这个过程确实复杂,详见下图
Q2: G function
这个题要求用函数实现一个数学方程G,类似于Fibonacci数列
一个定义在正整数上的数学函数G有两个情况:
G(n) = n, if n <= 3
G(n) = G(n - 1) + 2 * G(n - 2) + 3 * G(n - 3), if n > 3
要求分别用递归和迭代的方式实现
递归
def g(n):
"""Return the value of G(n), computed recursively.
"""
"*** YOUR CODE HERE ***"
if n <= 3:
return n
else:
return g(n-1) + 2 * g(n-2) + 3 * g(n-3)
def g_iter(n):
"""Return the value of G(n), computed iteratively.
"""
"*** YOUR CODE HERE ***"
if n <= 3:
return n
else:
pre_pre_previous = 1
pre_previous = 2
previous = 3
result = None
for i in range(4, n+1):
result = previous + 2 * pre_previous + 3 * pre_pre_previous
pre_pre_previous = pre_previous
pre_previous = previous
previous = result
return result
均满足评分脚本要求
$ python3 ok -q g --local
=====================================================================
Assignment: Homework 3
OK, version v1.18.1
=====================================================================
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Running tests
---------------------------------------------------------------------
Test summary
1 test cases passed! No cases failed.
$ python3 ok -q g_iter --local
=====================================================================
Assignment: Homework 3
OK, version v1.18.1
=====================================================================
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Running tests
---------------------------------------------------------------------
Test summary
1 test cases passed! No cases failed.
Q3: Missing Digits
实现一个递归函数missing_digits
该函数接受一个按递增顺序排列的数字n
- 12289符合要求
- 15362中第三位数字
3
小于第二位数字5
,不满足要求 - 98764中第二位数字
8
小于第一位数字9
,不满足要求
该函数返回n
中缺失数字的数量。
- 缺失数字是
n
的第一个和最后一个数字之间的数字,且其不在n
中
要求使用递归完成,若使用while或for循环,则无法通过测试
第一反应写的代码实现如下,这个函数实现是可以通过测试的(因为之前刚写过一个乒乓序列,顺着那个的思路也能写个差不多)
def missing_digits(n):
"""Given a number a that is in sorted, increasing order,
return the number of missing digits in n. A missing digit is
a number between the first and last digit of a that is not in n.
"""
"*** YOUR CODE HERE ***"
last = n % 10
all_but_last = n // 10
def help(count, remain, this):
if remain == 0:
return count
pre = remain % 10
count += max(this - pre - 1, 0)
return help(count, remain // 10, pre)
return help(0, all_but_last, last)
整理一下思路:
传入参数n
是一个整数,所以我们可以很方便的从右往左求得每一位数字
从数字第一位到最后一位缺失数字的数量 = 从数字第一位到倒数第二位缺失数字的数量 + 倒数第二位到最后一位之间缺失数字的数量
根据这个思路,我们可以用递归函数依次求最后两位之间缺失数字的数量
第一次调用,我们可以求倒数二和倒数一之间的缺失数字数量
第二次调用,求倒数第三位和倒数第二位之间缺失数字数量
如何减小问题规模?可以使用 // 10 操作来抛弃数字的最后一位,再将其传入递归函数
递归出口/基线条件在哪?当数字只有一位,那么只有两位了,我们只需要返回仅剩两位之间的缺失数字数量即可(或者数字仅剩一位了,那么肯定没有缺失数字,返回0即可)
至于如何算两个数字之间缺失数字的数量?思路如下
- 如果两数字相等,如88,两者之间肯定没有缺失数字
- 如果前一个数字比后一个数字少1,如78,那么两者之间也没有缺失数字
- 如果前一个数字比后一个数字少2以上,那么缺少
第二位 - 第一位 - 1
个数字,,如79,缺少9 - 7 - 1 = 1个数字
发现第二种也可记作第二位 - 第一位 - 1
,但第一种不行,两数字相等时按这个公式算得为-1,肯定不行,缺失数字得个数肯定是大于等于零的,所以归纳一下
缺失数字数量 = 较大值(0,第二位 - 第一位 - 1)
按照上述思路写一下代码:
先是def语句
def missing_digits(n):
写递归出口
出口版本1
if n < 100: # 仅剩两位数字了
last = n % 10
second_last = n // 10
return max(0, last-second_last-1)
出口版本2,这一个出口比上一版的要方便,因为你写完就会发现,如果是版本一,那么不管是出口内部,还是出口外部,都会进行相同的操作
if n < 10:
return 0
写正常递归的部分
last = n % 10 # 提取数字最后一位
second_last = n // 10 % 10 # 提取数字倒数第二位
remain = n // 10 # 缩小问题规模
return missing_digits(remain) + max(0, last-second_last-1)
连起来就是
def missing_digits(n):
if n < 10:
return 0
last = n % 10 # 提取数字最后一位
second_last = n // 10 % 10 # 提取数字倒数第二位
remain = n // 10 # 缩小问题规模
return missing_digits(remain) + max(0, last-second_last-1)
也可以通过打分脚本的测评
$ python3 ok -q missing_digits --local
=====================================================================
Assignment: Homework 3
OK, version v1.18.1
=====================================================================
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Running tests
---------------------------------------------------------------------
Test summary
1 test cases passed! No cases failed.
Q4: Count change
课程对题目的描述如下:
当机器接管后,每个硬币的面值将是2的幂次方:1美分,2美分,4美分,8美分,16美分等等。硬币的价值不受限制。
给定一个正整数total,如果硬币的价值之和等于total,则硬币集合可以为total找零。例如,以下集合可以为7找零:
7枚1美分硬币
5枚1美分、1枚2美分硬币
3枚1美分、2枚2美分硬币
3枚1美分、1枚4美分硬币
1枚1美分、3枚2美分硬币
1枚1美分、1枚2美分、1枚4美分硬币
因此,有6种方法可以为7找零。编写一个递归函数count_change,该函数采用正整数total并返回使用未来硬币进行找零的方法数。
提示:参考count_partitions的实现,了解如何使用较小的部分计算总和的方法。如果需要在递归调用之间跟踪多个值,请考虑编写一个辅助函数。
最初思路如下:
首先要知道所有可能拼凑成total
的硬币的面值(在本题中,硬币的面值为2的次方,有无限个),即硬币的面值
例如total
的值为7,那么硬币的面值只能为4
,2
,1
为拼凑零钱,首先我们从上述面值的硬币中选一枚
选择面值为4
的硬币,然后拼凑总和为7 - 4 = 3
的零钱,且剩余零钱只能从面值小于等于4中选
选择面值为2
的硬币,然后拼凑总和为7 - 2 = 5
的零钱,且剩余零钱只能从面值小于等于2中选
选择面值为1
的硬币,然后拼凑总和为7 - 1 = 6
的零钱,且剩余零钱只能从面值小于等于1中选
注意!上述不同选择中,选择一个硬币后,对之后的硬币选择作出面值限制是为了防止方案重复!!!
- 选4,总和为4
- 选4,总和大于7,方案数加零
- 选2,总和为6
- 选2,总和大于7,方案数加零
- 选1,总和等于7,方案数加一
- 选1,总和为5
- 选1,总和为6
- 选1,总和为7,方案数加一
- 选1,总和为6
- 选2,总和为2
- 选2,总和为4
- 选2,总和为6
- 选2,总和大于7,方案数加零
- 选1,总和为7,方案书加一
- 选1,总和为5
- 选1,总和为6
- 选1,总和为7,方案数加一
- 选1,总和为6
- 选2,总和为6
- 选1,总和为3
- 选1,总和为4
- 选1,总和为5
- 选1,总和为6
- 选1,总和为7,方案数加一
- 选1,总和为6
- 选1,总和为5
- 选1,总和为4
- 选2,总和为4
- 一直选1,知道总和为7,方案数加一
一共六种,可以避免重复,但是若要第一枚硬币分别选出所有情况,那还是无法避免使用循环来辅助(如循环,4-2-1),参考count_partions
的实现,新的思路如下:
整个过程可以有如下描述
- 选择面值为4的硬币,对剩余
7 - 4 = 3
用面值小于等于4的硬币分割 - 用面值小于
4
(即面值不超过2
)的硬币对7
进行分割
与count_partions
不同的是,找零问题将分割目标值的数字限制为特殊值,
首先我们要找到可以分割n
的最大硬币面值,可以使用如下方法
def find_max_part(n):
from math import log2
assert n >= 0
return 2 ** int(log2(n))
按照count_partions
的思路实现函数
def count_change(n):
def helper(n, m):
if n == 0:
return 1
elif n < 0:
return 0
if m <= 0:
return 0
return helper(n-m, m) + helper(n, m // 2)
def find_max_part(n):
from math import log2
assert n >= 0
return 2 ** int(log2(n))
return helper(n, find_max_part(n))
上述实现符合要求
$ python3 ok -q count_change --local
=====================================================================
Assignment: Homework 3
OK, version v1.18.1
=====================================================================
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Running tests
---------------------------------------------------------------------
Test summary
1 test cases passed! No cases failed.
也可以从面值为1的硬币算起,这种情况不需要计算最大面值,只需要改一下递归出口和递归调用就可以
def count_change(total):
def helper(n, m):
if n == 0:
return 1
elif n < 0:
return 0
if m > total:
return 0
return helper(n-m, m) + helper(n, m * 2)
return helper(total, 1)
以上实现也能通过脚本测评
Q5: Towers of Hanoi
汉诺塔,由三个柱子和一些不同大小的盘子组成,这些盘子可以滑动到其他柱子上
将n个盘子以递增的顺序摆在在起始柱子(即最小的盘子在顶部,最大的盘子在底部,形成一个圆锥形)
谜题的目标是将圆锥移动到一个目标柱子上,遵守以下规则:
- 一次只能移动一个盘子。
- 每次移动包括从一个柱子上取下顶部(最小的)盘子,滑动到另一个柱子上,放在已经存在的其他盘子上面。
- 不能将盘子放在比它更小的盘子上面。
要求实现函数move_stack
,它会打印出将n个盘子从起始柱子移动到目标柱子所需的每一步
课程组已提供print_move
函数用于将打印将单个盘子从给定起点移动到给定终点的步骤。
我们将问题的规模从1增大到3来看一下如何解决这个问题
n = 1时:
只需要将唯一一个盘子从起始柱子移动到目标柱子上即可
n = 2时:
- 将较小的盘子从起始柱子移动到中间柱子上
- 将较大的盘子从起始柱子移动到目标柱子上
- 将中间柱子上的一个盘子移动到目标柱子上
n = 3时:
- 将上面两个盘子从起始柱子移动到中间柱子上(这个过程等同于n = 2时汉诺塔问题的过程)
- 将最大的盘子从起始柱子移动到目标柱子上
- 将中间柱子上的两个盘子移动到目标柱子上(这个过程等同于n = 2时汉诺塔问题的过程)
我们发现,当问题规模大于2时,解决该问题总是可以分为三个步骤:
- 将除了最大的盘子外的所有盘子移动到中间柱子上
- 将最大的盘子从起始柱子移动到目标柱子上
- 将中间柱子上的所有盘子移动到目标柱子上
其中第一步和第三步又相当于问题规模减一后的汉诺塔问题,而基线条件/基本情况即n = 1
的情况
函数的实现如下(其中print_move
函数已由课程组给出):
def print_move(origin, destination):
"""Print instructions to move a disk."""
print("Move the top disk from rod", origin, "to rod", destination)
def move_stack(n, start, end):
"""Print the moves required to move n disks on the start pole to the end
pole without violating the rules of Towers of Hanoi.
n -- number of disks
start -- a pole position, either 1, 2, or 3
end -- a pole position, either 1, 2, or 3
There are exactly three poles, and start and end must be different. Assume
that the start pole has at least n disks of increasing size, and the end
pole is either empty or has a top disk larger than the top n start disks.
"""
assert 1 <= start <= 3 and 1 <= end <= 3 and start != end, "Bad start/end"
"*** YOUR CODE HERE ***"
if n == 1: # 基线条件
print_move(start, end)
return
middle = 6 - start - end # 中间柱子
move_stack(n-1, start, middle)
move_stack(1, start, end)
move_stack(n-1, middle, end)
以上函数实现符合测评脚本要求
$ python3 ok -q move_stack --local
=====================================================================
Assignment: Homework 3
OK, version v1.18.1
=====================================================================
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Running tests
---------------------------------------------------------------------
Test summary
1 test cases passed! No cases failed.
Q6: Anonymous factorial
Y-组合子,我不会,难蚌,但是chat-gpt
当使用 Y-组合子来实现阶乘函数factorial时,我们需要首先定义一个阶乘函数的“不动点”,即一个函数f,它满足 f(n) = n * f(n-1) 对于任意自然数 n 都成立,而不需要递归调用自身。
具体而言,我们可以定义一个“不动点”函数F,它接受一个函数f作为参数,返回一个新的函数g,它是f的“不动点”。也就是说,g(n) = f(g)(n) 对于任意自然数 n 都成立。然后我们可以使用 Y-组合子将F转换成一个递归函数。
下面是具体实现的代码:
Y = lambda f: (lambda x: f(lambda v: x(x)(v))) (lambda x: f(lambda v: x(x)(v))) def F(f): def g(n): if n == 0: return 1 else: return n * f(n-1) return g factorial = Y(F) print(factorial(5)) # 输出 120
让我们逐步分解这个实现:
- 定义Y-组合子:
Y = lambda f: (lambda x: f(lambda v: x(x)(v))) > (lambda x: f(lambda v: x(x)(v)))
这个定义中,我们使用了lambda函数来创建一个匿名函数,它接受一个函数f作为参数。在这个函数内部,我们定义了另一个lambda函数x,它接受一个参数v,并返回x(x)(v)。这里x(x)实际上是递归地调用了自身,并返回一个新的函数,它将参数v传递给了x(x)。这个新的函数就是我们想要的Y-组合子。
- 定义“不动点”函数F:
def F(f): def g(n): if n == 0: return 1 else: return n * f(n-1) return g
这个函数接受一个函数f作为参数,返回一个新的函数g,它是f的“不动点”。在这个具体实现中,我们定义了一个递归>的阶乘函数,它接受一个自然数n作为参数,并返回n的阶乘。注意,这个阶乘函数并没有直接调用自身,而是通过f来实现递归调用。
- 使用Y-组合子将F转换为递归函数factorial:
factorial = Y(F)
这里我们使用Y-组合子将F转换为递归函数factorial。注意,我们没有显式地定义递归函数,而是通过Y-组合子来创建一个递归版本的F函数。现在,factorial就是一个递归的阶乘函数,可以直接调用。
虽然chat-gpt,但还是不懂
总而言之,将F的定义转换为lambda表达式,代码如下:
def make_anonymous_factorial():
"""Return the value of an expression that computes factorial.
"""
return (lambda f: (lambda x: f(lambda v: x(x)(v))) (lambda x: f(lambda v: x(x)(v))))(lambda f: lambda n: 1 if n == 1 else n * f(n-1))
可以通过脚本测试,百般武道,此乃chat偷鸡(落泪)