LeetCode:动态规划【基础题目求解】

2023-10-31

PS. 本文是参考代码随想录做的一些笔记,完整版本请戳链接,非常好的教程!

如果某一问题有很多重叠子问题,使用动态规划是最有效的。所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优。

例如:有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i]。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

动态规划中dp[j]是由dp[j-weight[i]]推导出来的,然后取max(dp[j], dp[j - weight[i]] + value[i])。贪心是每次拿物品选一个最大的或者最小的就完事了,和上一个状态没有关系。

动态规划的解题步骤:

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

\quad
\quad
下面来做一些基础的题目~

509. 斐波那契数

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是:

  • F(0) = 0,F(1) = 1
  • F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n) 。


方法一:简单递归

class Solution:
    def fib(self, n: int) -> int:
        if n < 2: return n
        return self.fib(n - 1) + self.fib(n - 2)

方法二:DP

class Solution:
    def fib(self, n: int) -> int:
        if n < 2: return n
        dp = [0] * (n + 1)
        dp[0] = 0
        dp[1] = 1

        for i in range(2, n + 1):
            dp[i] = dp[i - 1] + dp[i - 2]

        return dp[n]

这道题很简单,没啥可说的,可以看到DP方法花费的时间更少。

70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?


这道题其实就是斐波那契数列!!

爬到第一层楼梯有一种方法,爬到二层楼梯有两种方法。那么第一层楼梯再跨两步就到第三层 ,第二层楼梯再跨一步就到第三层。所以到第三层楼梯的状态可以由第二层楼梯和到第一层楼梯状态推导出来,那么就可以想到动态规划了。

确定dp数组以及下标的含义:dp[i]:爬到第i层楼梯,有dp[i]种方法。

递推公式:从dp[i]的定义可以看出,dp[i]可以由两个方向推出来。

  • 首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶就是dp[i]
  • 还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,那么再一步跳两个台阶就是dp[i]

所以dp[i] = dp[i - 1] + dp[i - 2]

class Solution:
    def climbStairs(self, n: int) -> int:
        if n <= 2: return n
        dp = [0] * (n + 1)
        dp[1] = 1
        dp[2] = 2
        for i in range(3, n + 1):
            dp[i] = dp[i - 1] + dp[i - 2]
        return dp[n]

746. 使用最小花费爬楼梯

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。


示例1中只花费一个15 就可以到阶梯顶,最后一步可以理解为不用花费,这点很重要。

dp[i]的定义:

  • 到达第i个台阶所花费的最少体力为dp[i]

递推公式:

  • 可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2]。一定是选花费最小的,所以dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]。(题目中的每当你爬上一个阶梯你都要花费对应的体力值,所以这里是cost[i]
class Solution:
    def minCostClimbingStairs(self, cost: List[int]) -> int:
        dp = [0] * len(cost)
        dp[0] = cost[0]
        dp[1] = cost[1]
        
        for i in range(2, len(cost)):
            dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]

        return min(dp[len(cost) - 1], dp[len(cost) - 2])

62. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?


dp[i][j]定义:表示从(0, 0)出发,到(i, j)dp[i][j]条不同的路径。

递推公式:dp[i][j] = dp[i - 1][j] + dp[i][j - 1],因为dp[i][j]只有这两个方向过来。

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [[0 for _ in range(n)] for _ in range(m)]

        for i in range(m):
            dp[i][0] = 1
        for j in range(n):
            dp[0][j] = 1

        for i in range(1, m):
            for j in range(1, n):
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1]

        return dp[m - 1][n - 1]
  • 时间复杂度:O(m × n)
  • 空间复杂度:O(m × n)

事实上,dp数组使用一个一维数组就可以解决问题了,可以优化点空间。怎么理解呢?可以看下图:
1.png
从上图中可以发现,按行看,每次循环更新dp数组时都是在上一行的基础上更新的,也就是说每次前一行其实都被覆盖了。所以用一个一维数组就可以解决问题了。

class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        dp = [1 for _ in range(n)]

        for i in range(1, m):
            for j in range(1, n):
                dp[j] += dp[j - 1]
                
        return dp[n - 1]
  • 时间复杂度:O(m × n)
  • 空间复杂度:O(n)

方法三:数论方法

一共 m m m行, n n n列的话,可以看到,从起点到终点,至少需要走 m + n − 2 m+n-2 m+n2步。在这 m + n − 2 m + n - 2 m+n2步中,一定有 m − 1 m - 1 m1 步是要向下走的,不用管什么时候向下走。那么有几种走法呢? 可以转化为,有 m + n − 2 m + n - 2 m+n2个不同的数,随便取 m − 1 m - 1 m1个数,有几种取法。这不就是一个组合问题吗?也就是一共有 C m + n − 2 m − 1 C_{m+n-2}^{m-1} Cm+n2m1种方法,计算公式知道吧?直接开写!

class Solution:
  def uniquePaths(self, m: int, n: int) -> int:
        numerator = 1  # 分子
        denominator = 1  # 分母
        count = m - 1
        t = m + n - 2
        # 计算分子
        while count:
            count -= 1
            numerator *= t
            t -= 1

        # 计算分母
        for i in range(1, m):
            denominator *= i

        return int(numerator / denominator)

Python不用考虑溢出的问题,所以比较简单,直接照着公式算就行了。

63. 不同路径 II

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?网格中的障碍物和空位置分别用 1 和 0 来表示。


dp[i][j]的定义:表示从(0, 0)出发,到(i, j)dp[i][j]条不同的路径。

递推公式:dp[i][j] = dp[i - 1][j] + dp[i][j - 1]

注意:(i, j)如果就是障碍的话应该就保持初始状态(初始状态为0)。

class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        m, n = len(obstacleGrid), len(obstacleGrid[0])
        dp = [[0 for _ in range(n)] for _ in range(m)]
        
        # 初始化第一列
        for i in range(m):
            if obstacleGrid[i][0] == 1: break
            dp[i][0] = 1
        
        # 初始化第一行
        for j in range(n):
            if obstacleGrid[0][j] == 1: break
            dp[0][j] = 1

        for i in range(1, m):
            for j in range(1, n):
                if obstacleGrid[i][j] == 0:
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1]

        return dp[m - 1][n - 1]
  • 时间复杂度:O(n × m),n、m 分别为obstacleGrid 长度和宽度
  • 空间复杂度:O(n × m)

343. 整数拆分

给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。返回 你可以获得的最大乘积 。


dp[i]:分拆数字i,可以得到的最大乘积为dp[i]

递推公式:从1遍历j,然后有两种渠道得到dp[i]

  • 一个是j * (i - j) 直接相乘。
  • 一个是j * dp[i - j],相当于是拆分(i - j)

j是从1开始遍历,在遍历j的过程中j的拆分都计算过了。那么从1遍历j,比较(i - j) * jdp[i - j] * j取最大的。所以,递推公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j))

注意:从dp[i]的定义来说,dp[0]dp[1]就不应该初始化,也就是没有意义的数值。

class Solution:
    def integerBreak(self, n: int) -> int:
        dp = [0 for _ in range(n + 1)]
        dp[2] = 1
        for i in range(3, n + 1):
            for j in range(1, i - 1):
                dp[i] = max(dp[i], max(j * dp[i - j], j * (i - j)))
        return dp[n]

96. 不同的二叉搜索树(❤❤❤)

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
image.png


**dp[i]**定义:1到**i**为节点组成的二叉搜索树的个数为**dp[i]**

递推公式就有点复杂了,下面来具体看看:

对于 n = 1 n=1 n=1的情况:
image.png
很明显,只有一种情况。

对于 n = 2 n=2 n=2的情况:
image.png
这也很明显,只有两种情况。我们可以根据头节点拆分成两种情况:

  • 元素1为头节点:左子树节点个数为0(1种情况),右子树节点个数为1(1种情况);
  • 元素2为头节点:左子树节点个数为1(1种情况),右子树节点个数为0(1种情况);

为啥要这样看?这是为了和后面的情况进行推导。(很难想!)

对于 n = 3 n=3 n=3的情况:
image.png
这个不是很好看,我们根据头节点拆分成三种情况:

  • 元素1为头节点:左子树节点个数为0(1种情况),右子树节点个数为2(2种情况);
  • 元素2为头节点:左子树节点个数为1(1种情况),右子树节点个数为1(1种情况);
  • 元素3为头节点:左子树节点个数为2(2种情况),右子树节点个数为0(1种情况);

也就是说,我们可以得到如下结论:

  • 元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 * 左子树有0个元素的搜索树数量
  • 元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 * 左子树有1个元素的搜索树数量
  • 元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量

此外,我们可以知道:

  • 有2个元素的搜索树数量就是dp[2]
  • 有1个元素的搜索树数量就是dp[1]
  • 有0个元素的搜索树数量就是dp[0]

回顾一下dp数组的定义,dp[3]就是元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量。即:
dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]

根据这样的想法,我们可以推导初如下递推公式:dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]j相当于是头结点的元素,从1遍历到i为止。所以递推公式:dp[i] += dp[j - 1] * dp[i - j]j-1j为头结点左子树节点数量,i-j为以j为头结点右子树节点数量。

从递推公式可以看出,推导的基础是dp[0]。所以只需要初始化dp[0]即可。空节点也是一颗二叉搜索树,所以初始化dp[0]=1没毛病!

从递归公式上来讲,dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] 中以j为头结点左子树节点数量为0,也需要dp[以j为头结点左子树节点数量] = 1, 否则乘法的结果就都变成0了。

代码实现:

class Solution:
    def numTrees(self, n: int) -> int:
        dp = [0] * (n + 1)
        dp[0] = 1
        for i in range(1, n + 1):
            for j in range(1, i + 1):
                dp[i] += dp[j - 1] * dp[i - j]
        return dp[n]
  • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度: O ( n ) O(n) O(n)

\quad
\quad
\quad
\quad

持续更新中…

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

LeetCode:动态规划【基础题目求解】 的相关文章

  • 在 python 程序中合并第三方库的最佳实践是什么?

    下午好 我正在为我的工作编写一个中小型Python程序 该任务需要我使用 Excel 库xlwt and xlrd 以及一个用于查询 Oracle 数据库的库 称为CX Oracle 我正在通过版本控制系统 即CVS 开发该项目 我想知道围
  • Python 的键盘中断不会中止 Rust 函数 (PyO3)

    我有一个使用 PyO3 用 Rust 编写的 Python 库 它涉及一些昂贵的计算 单个函数调用最多需要 10 分钟 从 Python 调用时如何中止执行 Ctrl C 好像只有执行结束后才会处理 所以本质上没什么用 最小可重现示例 Ca
  • SQLAlchemy 通过关联对象声明式多对多自连接

    我有一个用户表和一个朋友表 它将用户映射到其他用户 因为每个用户可以有很多朋友 这个关系显然是对称的 如果用户A是用户B的朋友 那么用户B也是用户A的朋友 我只存储这个关系一次 除了两个用户 ID 之外 Friends 表还有其他字段 因此
  • 将 saxon 与 python 结合使用

    我需要使用 python 处理 XSLT 目前我正在使用仅支持 XSLT 1 的 lxml 现在我需要处理 XSLT 2 有没有办法将 saxon XSLT 处理器与 python 一起使用 有两种可能的方法 设置一个 HTTP 服务 接受
  • OpenCV Python cv2.mixChannels()

    我试图将其从 C 转换为 Python 但它给出了不同的色调结果 In C Transform it to HSV cvtColor src hsv CV BGR2HSV Use only the Hue value hue create
  • Python - StatsModels、OLS 置信区间

    在 Statsmodels 中 我可以使用以下方法拟合我的模型 import statsmodels api as sm X np array 22000 13400 47600 7400 12000 32000 28000 31000 6
  • 如何使用Conda下载python包并随后离线安装?

    我知道通过 pip 我可以使用以下命令下载 Python 包 但 pip install 破坏了我的内部包依赖关系 当我做 pip download
  • 如何替换 pandas 数据框列中的重音符号

    我有一个数据框dataSwiss其中包含瑞士城市的信息 我想用普通字母替换带有重音符号的字母 这就是我正在做的 dataSwiss Municipality dataSwiss Municipality str encode utf 8 d
  • python 相当于 R 中的 get() (= 使用字符串检索符号的值)

    在 R 中 get s 函数检索名称存储在字符变量 向量 中的符号的值s e g X lt 10 r lt XVI s lt substr r 1 1 X get s 10 取罗马数字的第一个符号r并将其转换为其等效整数 尽管花了一些时间翻
  • 使用 Tkinter 显示 numpy 数组中的图像

    我对 Python 缺乏经验 第一次使用 Tkinter 制作一个 UI 显示我的数字分类程序与 mnist 数据集的结果 当图像来自 numpy 数组而不是我的 PC 上的文件路径时 我有一个关于在 Tkinter 中显示图像的问题 我为
  • Python pickle:腌制对象不等于源对象

    我认为这是预期的行为 但想检查一下 也许找出原因 因为我所做的研究结果是空白 我有一个函数可以提取数据 创建自定义类的新实例 然后将其附加到列表中 该类仅包含变量 然后 我使用协议 2 作为二进制文件将该列表腌制到文件中 稍后我重新运行脚本
  • 如何在Python中获取葡萄牙语字符?

    我正在研究葡萄牙语 角色看起来很奇怪 我怎样才能解决这个问题 代码 import feedparser import random Vou definir os feeds feeds conf feedurl http pplware s
  • 在Python中获取文件描述符的位置

    比如说 我有一个原始数字文件描述符 我需要根据它获取文件中的当前位置 import os psutil some code that works with file lp lib open path to file p psutil Pro
  • Pandas:merge_asof() 对多行求和/不重复

    我正在处理两个数据集 每个数据集具有不同的关联日期 我想合并它们 但因为日期不完全匹配 我相信merge asof 是最好的方法 然而 有两件事发生merge asof 不理想的 数字重复 数字丢失 以下代码是一个示例 df a pd Da
  • 将图像分割成多个网格

    我使用下面的代码将图像分割成网格的 20 个相等的部分 import cv2 im cv2 imread apple jpg im cv2 resize im 1000 500 imgwidth im shape 0 imgheight i
  • 如何在seaborn displot中使用hist_kws

    我想在同一图中用不同的颜色绘制直方图和 kde 线 我想为直方图设置绿色 为 kde 线设置蓝色 我设法弄清楚使用 line kws 来更改 kde 线条颜色 但 hist kws 不适用于显示 我尝试过使用 histplot 但我无法为
  • 每个 X 具有多个 Y 值的 Python 散点图

    我正在尝试使用 Python 创建一个散点图 其中包含两个 X 类别 cat1 cat2 每个类别都有多个 Y 值 如果每个 X 值的 Y 值的数量相同 我可以使用以下代码使其工作 import numpy as np import mat
  • 使用 Python 绘制 2D 核密度估计

    I would like to plot a 2D kernel density estimation I find the seaborn package very useful here However after searching
  • Python:如何将列表列表的元素转换为无向图?

    我有一个程序 可以检索 PubMed 出版物列表 并希望构建一个共同作者图 这意味着对于每篇文章 我想将每个作者 如果尚未存在 添加为顶点 并添加无向边 或增加每个合著者之间的权重 我设法编写了第一个程序 该程序检索每个出版物的作者列表 并
  • 导入错误:没有名为 site 的模块 - mac

    我已经有这个问题几个月了 每次我想获取一个新的 python 包并使用它时 我都会在终端中收到此错误 ImportError No module named site 我不知道为什么会出现这个错误 实际上 我无法使用任何新软件包 因为每次我

随机推荐

  • MacBook外接显示器设置方法(新手入门贴)

    小屏幕的MacBook MacBook Pro放在桌上长时间使用 眼睛比较累 而且 长时间低头看屏幕 易得颈椎病 绝对有损健康 配一台大屏幕的外置显示器不失为两全其美的好办法 首先 得买一台中意的大屏幕LED显示器 废话undefined
  • steam植物大战僵尸汉化补丁使用教程

    植物大战僵尸作为小时候印象最深的游戏之一 上线便收获了一大波人的喜爱与好评 仍至今日 还有许多小伙本们沉浸其中 不过steam版本并不支持简体中文语言 网络上面虽然一大堆但都是很久之前的 会出现一些黑屏的问题 所以小编此次带来了steam植
  • ILSVRC竞赛详细介绍(ImageNet Large Scale Visual Recognition Challenge)

    ILSVRC ImageNet Large Scale Visual Recognition Challenge 是近年来机器视觉领域最受追捧也是最具权威的学术竞赛之一 代表了图像领域的最高水平 ImageNet数据集是ILSVRC竞赛使用
  • QT使用ODBC连接MySQL

    本文主要讲述QT使用ODBC连接MySQL数据库的过程 第一步 下载连接工具 链接如下 https cdn mysql com Downloads Connector ODBC 8 0 mysql connector odbc 8 0 28
  • vmware17:下载安装

    1 访问vm官网下载vm17安装包 下载 VMware Workstation Pro CN 选择windows下载 下载成功后 双击装包安装 直接下一步安装 勾选接受许可条款下一步 更改安装路径 继续下一步 最后点击安装 最后等待完成输入
  • 线程的属性 —— 分离的状态(detached state)、栈地址(stack address)、栈大小(stack size)

    参考 四十二 线程 线程属性 作者 FadeFarAway 发布时间 2017 01 17 14 09 55 网址 https blog csdn net FadeFarAway article details 54576771 目录 引入
  • 科技云报道:生成式AI已成为企业新兴风险,但我们不应该因噎废食

    科技云报道原创 2023年 生成式AI技术破茧成蝶 引发了一场全球范围的数字革命 从最初的聊天 下棋开始 到医疗 金融 制造 教育 科研等 生成式AI表现出了强大的创造力和无限潜力 据不完全统计 截至今年8月底 全国已经发布了逾百个行业AI
  • cccccc

    Source code recreated from a class file by IntelliJ IDEA powered by FernFlower decompiler package com gb soa omp ccommon
  • idea找不到版本的可能性总结

    当spring boot starter parent下面的版本报红时并不是这个版本不存在 而是因为idea会默认缓存Maven本地仓库已存在的中的依赖项 只是我们引入的的父依赖版本 本地仓库中不存在 所以就报错了 解决方案就是我们清除一下
  • 【Pytorch with fastai】第 3 章 :数据伦理

    大家好 我是Sonhhxg 柒 希望你看完之后 能对你有所帮助 不足请指正 共同学习交流 个人主页 Sonhhxg 柒的博客 CSDN博客 欢迎各位 点赞 收藏 留言 系列专栏 机器学习 ML 自然语言处理 NLP 深度学习 DL fore
  • Linux 篇:Linux定时任务

    什么是crond crond是linux用来定期执行命令或指定程序任务的一种服务 安装完操作系统后 默认会启动crond任务调度服务 crond服务会定期检查系统中是否有要执行的任务 如果有要执行的任务便会自动执行该任务 crond定时任务
  • Oracle数据库DBA权限回收操作参考

    1 基本操作指令 查看当前系统 ORACLE SID linux su oracle cat etc oratab orcl oracle app oracle product 11 2 0 dbhome 1 N crm oracle ap
  • 网络层协议介绍

    网络层的功能 1 定义了基于IP协议的逻辑地址 2 链接不同的媒介类型 3 选择数据通过网络的最佳路径 IP数据包格式 数据封装的时候在网络层会封装ip地址的头部 形成ip数据包 IP数据包格式 分为20字节的固定部分 表示每个ip数 据包
  • python压缩文件夹下的文件及子文件夹

    压缩某一文件夹下的所有文件 python import os import zipfile def is child dir dir1 dir2 if dir1 dir2 return True return dir1 startswith
  • stat函数详解

    Linux系统函数之文件系统管理 二 stat函数 作用 获取文件信息 头文件 include
  • 【Matlab】基于SVM支持向量机的时间序列预测(Excel可直接替换数据)

    Matlab 基于SVM支持向量机的时间序列预测 Excel可直接替换数据 1 模型原理 2 文件结构 3 Excel数据 4 分块代码 5 完整代码 6 运行结果 1 模型原理 基于支持向量机 Support Vector Machine
  • HCIPR&S223-V2.5一些总结

    1 下列关于DHCP Server仿冒者攻击说法正确的是 多选 AC A DHCP Server仿冒者攻击通过仿冒DHCP服务器向终端下发错误的IP地址和网络参数 导致用户无法上网 B 华为交换机接口下开启DHCP snooping功能后默
  • elementui的轮播图

    需求是需要把左右切换分页的大小于号放在轮播图外面 涉及到了一些问题 1 实现思路 2 绝对定位垂直居中 3 需要覆盖一些组件里的样式 直接给设置样式无法覆盖 解决1 实现思路 没有找到插件原生的方法 于是看了一遍文章 制作两个div装着图片
  • R语言主成分分析

    head swiss 查看数据 cor swiss 查看相关性矩阵 方阵中绝对值最小的是0 06085861 比0 05大 因此swiss中变量相互之间均有或强或弱的相关关系 这份数据适合做主成份分析 由于变量的量纲不同会使主成份得分系数的
  • LeetCode:动态规划【基础题目求解】

    PS 本文是参考代码随想录做的一些笔记 完整版本请戳链接 非常好的教程 如果某一问题有很多重叠子问题 使用动态规划是最有效的 所以动态规划中每一个状态一定是由上一个状态推导出来的 这一点就区分于贪心 贪心没有状态推导 而是从局部直接选最优