递归(下)

683 阅读9分钟

递归的相关知识,内容包括分治策略、优化问题和贪心策略、找零兑换问题的递归解法、找零兑换问题的动态规划解法、动态规划案例分析、递归小结。

一、分治策略

1. 分治策略

解决问题的典型策略:分而治之

  • 将问题分为若干更小规模的部分
  • 通过解决每一个小规模部分问题,并将结果汇总得到原问题的解

2. 递归算法与分治策略

递归三定律:

  1. 基本结束条件,解决最小规模问题
  2. 缩小规模,向基本结束条件演进
  3. 调用自身来解决已缩小规模的相同问题

体现了分治策略

  • 问题解决依赖于若干缩小了规模的问题
  • 汇总得到原问题的解

应用相当广泛

  • 排序、查找、遍历、求值等等

二、优化问题和贪心策略

1. 优化问题

计算机科学中许多算法都是为了找到某些问题的最优解

  • 例如,两个点之间的最短路径;
  • 能最好匹配一系列点的直线;
  • 或者满足一定条件的最小集合

2. 找零兑换问题

一个经典案例是兑换最少个数的硬币问题

假设你为一家自动售货机厂家编程序,自动售货机要每次找给顾客最少数量硬币;

假设某次顾客投进$1纸币,买了ȼ37的东西,要找ȼ63,那么最少数量就是:2个quarter(ȼ25)、1个dime(ȼ10)和3个penny(ȼ1),一共6个

4. 贪心策略解决找零兑换问题

人们会采用各种策略来解决这些问题,例如最直观的“贪心策略”

一般我们这么做:

  • 从最大面值的硬币开始,用尽量多的数量
  • 有余额的,再到下一最大面值的硬币,还用尽量多的数量,一直到penny(ȼ1)为止

5. 贪心策略Greedy Method

贪心策略

  • 因为我们每次都试图解决问题的尽量大的一部分对应到兑换硬币问题,就是每次以最多数量的最大面值硬币来迅速减少找零面值
  • “贪心策略”解决找零兑换问题,在美元或其他货币的硬币体系下表现尚好

6. 贪心策略失效

但如果你的老板决定把自动售货机出口到Elbonia,事情就会有点复杂(系列漫画Dilbert里杜撰的国家),因为这个古怪的国家除了上面3种面值之外,还有一种【ȼ21】的硬币!

按照“贪心策略”,在Elbonia,ȼ63还是原来的6个硬币

ȼ63 = ȼ25*2 + ȼ10*1 + ȼ1*3

但实际上最优解是3个面值ȼ21的硬币!

ȼ63 = ȼ21*3

“贪心策略”失效了

三、找零兑换问题的递归解法

1. 找零兑换问题:递归解法

我们来找一种肯定能找到最优解的方法

  • 贪心策略是否有效依赖于具体的硬币体系

首先是确定基本结束条件,兑换硬币这个问题最简单直接的情况就是,需要兑换的找零,其面值正好等于某种硬币

  • 如找零25分,答案就是1个硬币!

其次是减小问题的规模,我们要对每种硬币尝试1次,例如美元硬币体系:

  • 找零减去1分(penny)后,求兑换硬币最少数量(递归调用自身);
  • 找零减去5分(nikel)后,求兑换硬币最少数量
  • 找零减去10分(dime)后,求兑换硬币最少数量
  • 找零减去25分(quarter)后,求兑换硬币最少数量上述4项中选择最小的一个。

2. 找零兑换问题:递归解法代码

3. 找零兑换问题:递归解法分析

递归解法虽然能解决问题,但其最大的问题是:极!其!低!效!

  • 对63分的兑换硬币问题,需要进行67,716,925次递归调用!
  • 在我这台笔记本电脑上花费了40秒时间得到解:6个硬币

以26分兑换硬币为例,看看递归调用过程(377次递归的一小部分)

我们发现一个重大秘密,就是重复计算太多!

  • 例如找零15分的,出现了3次!而它最终解决还要52次递归调用
  • 很明显,这个算法致命缺点是重复计算

4. 找零兑换问题:递归解法改进

对这个递归解法进行改进的关键就在于消除重复计算

  • 我们可以用一个表将计算过的中间结果保存起来,在计算之前查表看看是否已经计算过

这个算法的中间结果就是部分找零的最优解,在递归调用过程中已经得到的最优解被记录下来

  • 在递归调用之前,先查找表中是否已有部分找零的最优解
  • 如果有,直接返回最优解而不进行递归调用,如果没有,才进行递归调用

改进后的解法,极大减少了递归调用次数

  • 对63分兑换硬币问题,仅仅需要221次递归调用是改进前的三十万分之一,瞬间返回!

四、找零兑换问题的动态规划解法

1. 找零兑换:动态规划解法

中间结果记录可以很好解决找零兑换问题

实际上,这种方法还不能称为动态规划,而是叫做“memoization(记忆化/函数值缓存)”的技术提高了递归解法的性能

动态规划算法采用了一种更有条理的方式来得到问题的解

找零兑换的动态规划算法从最简单的“1分钱找零”的最优解开始,逐步递加上去,直到我们需要的找零钱数

在找零递加的过程中,设法保持每一分钱的递加都是最优解,一直加到求解找零钱数,自然得到最优解

递加的过程能保持最优解的关键是,其依赖于更少钱数最优解的简单计算,而更少钱数的最优解已经得到了。

问题的最优解包含了更小规模子问题的最优解,这是一个最优化问题能够用动态规划策略解决的必要条件。

采用动态规划来解决11分钱的兑换问题。

计算11分钱的兑换法,我们做如下几步:

  1. 首先减去1分硬币,剩下10分钱查表最优解是1
  2. 然后减去5分硬币,剩下6分钱查表最优解是2
  3. 最后减去10分硬币,剩下1分钱查表最优解是1

通过上述最小值得到最优解:2个硬币

2. 找零兑换:动态规划算法代码

3. 找零兑换:动态规划算法扩展

我们注意到动态规划算法的dpMakeChange并不是递归函数

  • 虽然这个问题是从递归算法开始解决,但最终我们得到一个更有条理的高效非递归算法

动态规划中最主要的思想是:

  • 从最简单情况开始到达所需找零的循环
  • 其每一步都依靠以前的最优解来得到本步骤的最优解,直到得到答案。

前面的算法已经得到了最少硬币的数量,但没有返回硬币如何组合

扩展算法的思路很简单,只需要在生成最优解列表同时跟踪记录所选择的那个硬币币值即可

在得到最后的解后,减去选择的硬币币值,回溯到表格之前的部分找零,就能逐步得到每一步所选择的硬币币值

4. 找零兑换:动态规划算法扩展代码

五、动态规划案例分析

1. 讨论:博物馆大盗问题

大盗潜入博物馆,面前有5件宝物,分别有重量和价值,大盗的背包仅能负重20公斤,请问如何选择宝物,总价值最高?

我们把m(i, W)记为:

  • 前i(1<=i<=5)个宝物中,组合不超过W(1<=W<=20) 重量,得到的最大价值
  • m(i, W)应该是m(i-1, W)和m(i-1, W-Wi)+vi两者最大值
  • 我们从m(1, 1)开始计算到m(5, 20)

2. 博物馆大盗问题:动态规划表格

3. 博物馆大盗问题:动态规划算法代码

4. 小结

上面我们用动态规划和递归分别解决了博物馆大盗问题

由于递归算法简洁直观,只要递归和记忆化应用得当,也能高效解决这类问题

同学们可以把本案例与找零兑换问题的递归和动态规划解法分别对比,找出其中的规律

六、递归小结

在本章我们研究了几种递归算法,表明了递归是解决某些具有自相似性的复杂问题的有效技术

递归算法“三定律”

  1. 递归算法必须具备基本结束条件
  2. 递归算法必须要减小规模,改变状态,向基本结束条件演进
  3. 递归算法必须要调用自身

某些情况下,递归可以代替迭代循环

递归算法通常能够跟问题的表达自然契合

递归不总是最合适的算法,有时候递归算法会引发巨量的重复计算

“记忆化/函数值缓存”可以通过附加存储空间记录中间计算结果来有效减少重复计算

如果一个问题最优解包括规模更小相同问题的最优解,就可以用动态规划来解决

「资料来源:数据结构与算法Python版-陈斌」