[Python][递归] UCB CS61a 2020 Homework03

提示:本文内容来源于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),该函数将gfunc复合,并返回一个新的funcfunc_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即可)

至于如何算两个数字之间缺失数字的数量?思路如下

  1. 如果两数字相等,如88,两者之间肯定没有缺失数字
  2. 如果前一个数字比后一个数字少1,如78,那么两者之间也没有缺失数字
  3. 如果前一个数字比后一个数字少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中选

注意!上述不同选择中,选择一个硬币后,对之后的硬币选择作出面值限制是为了防止方案重复!!!

  1. 选4,总和为4
    1. 选4,总和大于7,方案数加零
    2. 选2,总和为6
      1. 选2,总和大于7,方案数加零
      2. 选1,总和等于7,方案数加一
    3. 选1,总和为5
      1. 选1,总和为6
        1. 选1,总和为7,方案数加一
  2. 选2,总和为2
    1. 选2,总和为4
      1. 选2,总和为6
        1. 选2,总和大于7,方案数加零
        2. 选1,总和为7,方案书加一
      2. 选1,总和为5
        1. 选1,总和为6
          1. 选1,总和为7,方案数加一
    2. 选1,总和为3
      1. 选1,总和为4
        1. 选1,总和为5
          1. 选1,总和为6
            1. 选1,总和为7,方案数加一
  3. 一直选1,知道总和为7,方案数加一

一共六种,可以避免重复,但是若要第一枚硬币分别选出所有情况,那还是无法避免使用循环来辅助(如循环,4-2-1),参考count_partions的实现,新的思路如下:

整个过程可以有如下描述

  1. 选择面值为4的硬币,对剩余7 - 4 = 3用面值小于等于4的硬币分割
  2. 用面值小于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时:

  1. 将较小的盘子从起始柱子移动到中间柱子上
  2. 将较大的盘子从起始柱子移动到目标柱子上
  3. 将中间柱子上的一个盘子移动到目标柱子上

n = 3时:

  1. 将上面两个盘子从起始柱子移动到中间柱子上(这个过程等同于n = 2时汉诺塔问题的过程)
  2. 将最大的盘子从起始柱子移动到目标柱子上
  3. 将中间柱子上的两个盘子移动到目标柱子上(这个过程等同于n = 2时汉诺塔问题的过程)

我们发现,当问题规模大于2时,解决该问题总是可以分为三个步骤:

  1. 将除了最大的盘子外的所有盘子移动到中间柱子上
  2. 将最大的盘子从起始柱子移动到目标柱子上
  3. 将中间柱子上的所有盘子移动到目标柱子上

其中第一步和第三步又相当于问题规模减一后的汉诺塔问题,而基线条件/基本情况即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偷鸡(落泪)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值