从暴力递归到动态规划

last modify

概述

  • 从代码角度, 动态规划的实现一般都是通过迭代完成的 (递推公式); 而一般迭代过程都有与之对应的递归写法;

    这里默认动态规划都是基于迭代的实现. 实际上, 动态规划只是一种算法, 跟具体实现无关, 递归实现也是动态规划;

  • 递归过程一般与人的直接思考过程更接近, 因此如果对递归结构比较熟悉, 就能更容易的写出相应解法;

    动态规划的难点是递推过程 (经验和数学能力); 递归的难点是递归结构本身;

  • 递归的一个问题是如果存在重复计算, 会大幅降低性能, 此时有两个解决方法:

    1. 转换为迭代结构; 转化过程可以套用固定模板, 详见示例;

    2. 使用记忆化搜索, 即保存中间结果; 如果用 python 书写, 可以使用 functools.lru_cache 装饰器, 详见示例;

    3. 如何判断是否存在重复计算?

      将递归过程展开为树状结构,当发现某些具有相同参数的递归调用出现在多个不同节点时,说明存在重复调用;

    4. 什么情况下可以使用记忆化搜索?

      这个问题的本质, 实际上是判断问题本身是不是一个动态规划问题; 能用动态规划解决的问题必须具备无后效性, 只要具备这个特性, 就可以使用记忆化搜索;

      无后效性: 某阶段的状态一旦确定, 则此后过程的决策不再受此前各种状态及决策的影响.

经典问题

  • 原文总结了 4 种常见的递归过程,基本能覆盖大部分场景;

1. 自底向上的递归/迭代

  • 最常见的递归模型, 一般过程:

    1. 确定初始状态 (递归基), 即 $k=0$ 时刻的状态)

    2. 假设已知 $k-1$ 及其之前时刻的状态,推导 $k$ 时刻的状态;

    有时候可能是自顶向下, 本质是一样的;

示例 1: 跳台阶 (一维)

70. 爬楼梯 - 力扣(LeetCode)

  • 该模型下的一维问题几乎都是 "跳台阶" 问题的变体;

记忆化搜索 (不使用标准库)
迭代写法 (无滚动优化)

示例 2: 解码方法 (一维, "跳台阶" 变体)

91. 解码方法 - 力扣(LeetCode)

  • 有限制的 "跳台阶" 问题;

迭代写法

示例 3: N 个骰子的点数 (一维, "跳台阶" 变体)

剑指 Offer 60. n个骰子的点数 - 力扣(LeetCode)

  • 进阶版 "跳台阶", 相当于每次能跳的步数更多;

迭代写法

2. 多样本位置全对应的尝试模型

3. 范围内的尝试模型

4. 寻找业务限制的尝试模型

该类型原文只给了很难的几个例子, 感兴趣的可以去看原视频;

更多相关问题

algorithms/暴力递归与动态规划

参考资料

Last updated