alex_bn_lee

导航

【498】动态规划 —— Dynamic Programming

参考1:如何理解动态规划?

参考2:B站视频比较详细

参考3:详解动态规划算法

一、动态规划总结【参考3】

  动态规划是用来解决最优问题(也可以用来求方案数)的一种方法,或者说是一种思想。而解决问题的过程,需要经历多个阶段,每一阶段都可以看成一个子问题,每个子问题都对应着一组状态。

  使用动态规划一般会经历四个步骤:

  1. 定义原问题和子问题。
    子问题是和原问题相似但规模较小的问题。

  2. 定义状态。
    这里的状态大家可以认为就是某个函数的自变量,根据状态中包含的参数个数的不同,我们在编程时设置的DP数组的维度就不同。每个状态中的参数通常都能对应DP数组中某个元素的下标,而DP数组的元素就是这个状态对应的子问题的求解结果。

  3. 寻找状态转移方程。
    这一步往往是最难的,大家需要找到关于状态之间的某种转移关系,这个关系往往是一个递推式子,根据这个递推式我们才能一步一步计算出DP数组里面的元素。另外别忘了确定边界条件,也就是我们递推的初始条件。另外,如果一个问题能用动态规划方法求解,需要满足最优子结构和无后效性,我在讲解例题时回避掉了这一点,但这不代表它们并不重要,因为这块的严格证明非常困难,后续如果你有兴趣可以学习专门的算法课程。

  4. 编程实现。
    如果前三步大家逻辑都理顺了,那么编程不是大的问题。我们无非就是先初始化一个DP数组,再结合边界条件计算DP数组中的初始值,最后再利用循环来对DP数组进行迭代。一般DP数组中最后那个元素就是我们要解决的原问题的答案。

 

二、动态规划中的常见概念【参考3】

  这里我们还是以求解斐波那契数列来举例子,尽管它不算严格的动态规划:

  1. 子问题和原问题
    原问题就是你要求解的这个问题本身,子问题是和原问题相似但规模较小的问题(原问题本身就是子问题的最复杂的情形,即子问题的特例)。
    例如:要求F(10), 那么求出F(10)就是原问题,求出F(k)(K≤10)都是子问题。

  2. 状态
    状态就是子问题中会变化的某个量,可以把状态看成我们要求解的问题的自变量
    例如:我们要求的F(10),那么这里的自变量10就是一个状态。

  3. 状态转移方程
    能够表示状态之间转移关系的方程,一般利用关于状态的某个函数建立起来。类似于数列的递推公式
    例如:F(n)=F(n-1)+F(n-2),当n为>2的整数时;当n=1或2时,F(n)=1,这种最简单的初始条件一般称为边界条件,也被称为基本方程。

  4. DP数组(DP就是动态规划的缩写)
    DP数组也可以叫“子问题数组”,因为DP数组中的每一个元素都对应一个子问题的结果,DP数组的下标一般就是该子问题对应的状态
    例如:使用自底向上法编程求解时,我们定义的向量FF就可以看成一个DP数组,数组下标从1取到n,对应的元素从F(1)取到F(n)。

 

三、相关例子【参考2】

1. 斐波拉契数列

  • 状态:连续的整数
  • 状态转移方程:dp[i] = dp[i-1] + dp[i-2]
  • DP数组:dp = [0] * (n+1)

代码:

def fib(n):
    dp = [0] * (n + 1) 
    dp[1] = 1
    dp[2] = 1
    
    if n >= 3:
        for i in range(3, len(dp)):
            dp[i] = dp[i - 1] + dp[i - 2]
        
    return dp[n] 

 

2. 三角形的最小路径和

  • 状态:连续的整数(row)
  • 状态转移方程:dp[i][j] = dp[i][j] + min(dp[i+1][j], dp[i+1][j+1])
  • DP数组:dp = [[4, 1, 8, 3]] * (row)

代码:

triangle = [[2, 0, 0, 0],
            [3, 4, 0, 0],
            [6, 5, 7, 0],
            [4, 1, 8, 3]]

def traverse():
    row = 4

    dp = [[4, 1, 8, 3]] * row 

    for i in range(row-2, -1, -1):
        for j in range(len(triangle[i]) - 1):
            dp[i][j] += min(dp[i+1][j], dp[i+1][j+1])

    return dp[0][0]

 

3. 凑零钱

  给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。输入: coins =
[1, 2, 5], amount = 11,输出: 3 解释: 11 = 5 + 5 + 1 输入: coins = [2], amount = 3,输出: -1

  • 状态:连续的整数,从 0 一直道 amount
  • 状态转移方程:dp[i] = min(dp[i - coins[j]) + 1
  • DP数组:dp = [amount] * (amount + 1)
  • 将每个金额的组合数分别计算出来

代码:

coins = [1, 2, 5]
amount = 20

def exchange(amount, coins):
    dp = [amount] * (amount + 1) 
    dp[0] = 0 

    for i in range(1, len(dp)):
        tmp = [amount] * len(coins)
        for j in range(len(coins)):
            tmp[j] = dp[i - coins[j]] + 1
        dp[i] = min(tmp) 
    
    return dp[amount] 
    
exchange(amount, coins) 

 


早期内容!  

  动态规划与其说是一个算法,不如说是一个方法论,就是从开始,沿着状态方程(递推公式)一步步推导出所需要结果的过程,是一个结局问题的思路和方法。主要包括以下三个步骤:

  1. 建立状态转移方程
  2. 缓存并服用以往结果
  3. 按顺序从小往大算

posted on 2020-11-25 21:22  McDelfino  阅读(111)  评论(0编辑  收藏  举报