Python递归优化避免死循环:递归程序设计艺术(7)

递归的第二个问题是死循环。死循环的产生是因为递归假设时,参数并没有向递归边界靠近。下面看一个例子。

倒水问题

 假设有一大一小两个没有刻度的杯子,大小杯子的容量分别是5升和3升。假设可以供给无限量的水,请问用这两个杯子如何倒出4升水出来[1]

程序的参数是ab分别表示大小杯子中当前的水量。初值都是0。根据递归三步曲,递归边界显然是a == 4 or b == 4[2]。递归假设略。这一题的关键是如何进行递归推导。

因为当前参数的状态是(ab),所以有八种操作可以改变当前状态:

综上所述,给出代码如下:

解决倒水问题的递归程序(存在死循环)

class Pour:
    def __init__(self, A, B, G):
        self.A = A  # 大杯子容量
        self.B = B  # 小杯子容量
        self.G = G  # 目标量

    def pour(self, a=0, b=0):
        # a: 大杯子水量
        # b: 小杯子水量
        if a == self.G or b == self.G:
            return True
        if a > 0 and self.pour(0, b):
            return True
        if b > 0 and self.pour(a, 0):
            return True
        if a < self.A and self.pour(self.A, b):
            return True
        if b < self.B and self.pour(a, self.B):
            return True
        if a < self.A and a+b >= self.A and self.pour(self.A, a+b-self.A):
            return True
        if b < self.B and a+b >= self.B and self.pour(a+b-self.B, self.B):
            return True
        if a+b <= self.A and self.pour(a+b, 0):
            return True
        if a+b <= self.B and self.pour(0, a+b):
            return True
        return False

if __name__ == '__main__':
    p = Pour(5, 3, 4)
    print(p.pour())

代码中使用了Python的类。类是对象的模版。理解类必须先理解对象。对象是数据和操作数据的函数的封闭环境。本题要编写一个倒水的递归函数,该函数需要5个参数:当前大小杯子中的水量、大小杯子的容量和目标水量。其中大小杯子的容量和目标水量在函数运行时是不会改变的,直接放在形参列表中显得多余,这就提示我们用一个封闭环境封装这三个数据,而递归函数pour将在这个环境中定义,从而使得它可以存取那三个数据。这就是类Pour的由来。

语句class Pour:中class是关键字表示定义一个类,冒号之后定义成员函数。__init__()是构造函数的名字,表示创建Pour类型对象时应该调用的函数。该函数带有四个参数,其中self表示当前正在被创建的对象。所有成员函数的第一个参数都必须是self,这不但区分了成员函数和全局函数,而且还便于成员函数存取封装在对象中的数据。ABG分别是大小杯子的容量和目标水量。构造函数体中有是三个形如self.A = A的赋值操作,表示对象定义了三个成员变量,名字也分别叫ABG,值分别等于参数ABG的值。这样就实现了Pour对象对大小杯子的容量和目标水量这三个数据的封装。

接下来定义的成员函数pour才是解决倒水问题的核心。由于上述理由,除了self之外该函数只需当前大小杯子中的水量这两个参数即可。其他三个数据从对象的成员变量中取得即可。为了方便调用方的调用,代码还设置这两个参数的缺省值为0。

pour函数体中,第一个if语句是判断递归边界,剩下的8个if语句是对8个倒水操作的模拟,每个倒水操作都会导致一个递归调用。

运行这个程序会发现程序报告RecursionError: maximum recursion depth exceeded in comparison即递归深度越界错。原因就在于,每一次倒水操作的确会改变当前参数的状态,比如(ab)(a, 3),但是不能保证新的状态(a, 3)会更靠近递归边界(即大小杯子中有一个杯子的水量接近4),从而导致递归不能正常终止。Python递归深度是有限制的[3],超过这个限制就会报上述错误。 

解决这个问题的办法与避免重复递归类似,用一个集合s保存参数的状态,如果当前的状态曾经出现过,则意味着程序出现了死循环,此时应该直接退出程序即可。代码如下:

避免倒水问题中的死循环

class Pour:
    def __init__(self, A, B, G):
        self.A = A  # 大杯子容量
        self.B = B  # 小杯子容量
        self.G = G  # 目标量

    def pour(self, a=0, b=0, s:set=None):
        # a: 大杯子水量
        # b: 小杯子水量
        if a == self.G or b == self.G:
            return True

        if s is None:
            s = set()   # 空集合
        else:
            key = (a, b)
            if key in s:    # 如果出现死循环
                return False
            s.add(key)

        if a > 0 and self.pour(0, b, s):
            return True
        if b > 0 and self.pour(a, 0, s):
            return True
        if a < self.A and self.pour(self.A, b, s):
            return True
        if b < self.B and self.pour(a, self.B, s):
            return True
        if a < self.A and a+b >= self.A and self.pour(self.A, a+b-self.A, s):
            return True
        if b < self.B and a+b >= self.B and self.pour(a+b-self.B, self.B, s):
            return True
        if a+b <= self.A and self.pour(a+b, 0, s):
            return True
        if a+b <= self.B and self.pour(0, a+b, s):
            return True
        return False

if __name__ == '__main__':
    p = Pour(5, 3, 4)
    print(p.pour())

其中pour()函数的第三个参数说明是s:set=None,冒号(:)用来说明参数s的类型,等号用来说明s的缺省值。注意,Python不是一个强类型语言,所以参数的类型是可以不说明的。这里说明参数类型的目的是使程序员明确该参数的类型以避免对其采取错误的操作。

Java、C、C++是强类型语言,强类型语言的优点是程序在编译阶段就能发现很多语法错误,比如成员方法使用错误等。所以强类型语言特别适合计算机高级语言的初学者,以便规范地使用变量和参数。但是强类型语言有两个限制:

  1. 变量和参数必须先声明后使用。
  2. 形参和实参的类型必须一致或者至少是相容的[4]

这些限制对初学者来说是有必要的,但对有经验的程序员来说就有些繁琐。更重要的是,有时并不能事先确定变量或者参数的类型,这样就限制了程序员的编程。

倒水办法

上述代码只回答了能否倒出4升水的问题,没有回答怎么倒的问题。为了解决这个问题可以在参数列表中增加一个字典(dict),用来保存当前状态来自于哪个状态。由于上述代码中本来就有一个集合(set)类型的参数s,它就是以参数状态为键的,所以可以把它升级为字典,以保存每个状态的父状态,同时仍然保留其避免递归死循环的作用。

除了s之外,还要增加一个参数parent以表明当前状态的父状态是谁。代码如下:

解决怎么倒水问题

class Pour:
    def __init__(self, A, B, G):
        self.A = A  # 大杯子容量
        self.B = B  # 小杯子容量
        self.G = G  # 目标量

    def pour(self, a=0, b=0):
        s = {}
        result = self._pour(a, b, s)
        path = []
        while result is not None:
            path.insert(0, result)
            result = s[result]
        return None if len(path) == 0 else path

    def _pour(self, a=0, b=0, s:dict=None, parent=None):
        # a: 大杯子水量
        # b: 小杯子水量
        # s: s的类型从set改为dict
        # parent: 当前状态的父状态
        key = (a, b)
        if a == self.G or b == self.G:
            s[key] = parent
            return key

        if s is None:
            s = {}   # 空字典
        elif key in s:    # 如果出现死循环
            return None   # 返回None表示没有解

        s[key] = parent
        if a > 0:
            result = self._pour(0, b, s, key)
            if result is not None:
                return result
        if b > 0:
            result = self._pour(a, 0, s, key)
            if result is not None:
                return result
        if a < self.A:
            result = self._pour(self.A, b, s, key)
            if result is not None:
                return result
        if b < self.B:
            result = self._pour(a, self.B, s, key)
            if result is not None:
                return result
        if a < self.A and a+b >= self.A:
            result = self._pour(self.A, a+b-self.A, s, key)
            if result is not None:
                return result
        if b < self.B and a+b >= self.B:
            result = self._pour(a+b-self.B, self.B, s, key)
            if result is not None:
                return result
        if a+b <= self.A:
            result = self._pour(a+b, 0, s, key)
            if result is not None:
                return result
        if a+b <= self.B:
            result = self._pour(0, a+b, s, key)
            if result is not None:
                return result
        return None

if __name__ == '__main__':
    p = Pour(5, 3, 4)
    result = p.pour()
    if result is None:
        print('没有办法倒出那么多水')
    else:
        print(result)

代码中Pour类里定义了.pour()和._pour()两个成员函数,其中后者是递归函数,是解决问题的主要函数。前者是对后者的一个包装,起两个作用:第一,避免调用方考虑如何为参数s和parent提供实参;第二,当调用后者成功之后,可以通过字典s找到倒水的完整路径。

._pour()函数作了较大修改,首先,返回值从bool型改为元组。该元组表示倒水的最后状态。其次,参数s的类型从set改为dict。最后,还增加了一个参数parent已表明当前状态来自于哪个状态。

运行这个程序的结果是:

[(0, 0), (5, 0), (5, 3), (0, 3), (3, 0), (3, 3), (5, 1), (0, 1), (1, 0), (1, 3), (4, 0)]

表明了从两个空杯子开始如何倒出4升水的整个过程。

 

[1] 与五猴分桃问题一样,倒水问题实际有数学解。但这里不必关心其数学解,读者应关注于如何用递归方法解决一个看似困难的问题。

[2] 虽然小杯子中是不可能有4升水的,但是为了让程序适应不同的参数值,这里暂且加上这个条件。

[3] 参见sys.getrecursionlimit()和sys.setrecursionlimit()函数

[4] 比如整型一般就与浮点型相容。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

方林博士

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值