柳小葱的力扣之路——动态规划详解

Posted 柳小葱

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了柳小葱的力扣之路——动态规划详解相关的知识,希望对你有一定的参考价值。

🌰今天写的这篇动态规划,算是对上一篇的补充,在上一篇的动态规划中,我们主要讲述了动态规划的基本概念和在一些简单题的应用,在这一期中,我们将详细讲述,动态规划适合求解那一类问题和递归有啥不同求解动态规划问题的套路是什么?对上一期内容感兴趣的小伙伴可以查看下面👇:

💝我们主要将从套路出手,解决一一些算法题中常见的动态规划问题。希望看完之后,让给你对动态规划有更深的理解。

1. 动态规划的套路

动态规划的求解的套路主要有下面三个步骤

  1. 找到问题的状态 i
  2. 明确dp[i]数组的含义
  3. 寻找状态i ,i-1,…之间的关系

碰见动态规划问题,最好从以上三个部分下手,因为这样子能够很好的抓住动态规划的本质。

还需要注意的是,动态规划求解方法主要分为以下3种:

  1. 暴力的递归解法
  2. 带记录的递归解法
  3. 迭代的动态规划求解法

2. 动态规划详解

在算法题中,动态规划问题的一般形式就是求最值。比如说让你求最长递增子序列呀,最小编辑距离呀等等。解决动态规划的核心问题是穷举,我们穷举出所有的答案,再在其中寻找最值即可,但穷举的时候需要注意下面几个问题:

  1. 问题存在大量重复的计算,暴力穷举效率低下
  2. 具备最优子结构
  3. 列出正确的状态转移方程

在这些问题中如何写出正确的状态转移方程是最需要注意的问题。

斐波那契数列指的是这样一个数列:
1,1,2,3,5,8,13,21,34,…
这个数列从第3项开始,每一项都等于前两项之和。

我们一般用递归实现的方式:

def fei(n):
    if n==1 or n==2:
        return 1
    return fei(n-1)+fei(n-2)

遇到递归问题,最好画出递归树:

我们发现,在计算f(19)和f(18)时,会出现f(17)重复计算的情况,把这棵树画完整,我们会发现大量重复计算的节点。于是,这就出现了动态规划问题的第一个特点:重叠子问题

既然每次计算都需要进行大量重复的工作,那我们可以创建一个记录表,对需要计算的子问题先去记录表中寻找答案,如果没有,再利用记录已有的数据直接进行计算。

#动态规划算法
def fei(n):
    dp=[0 for i in range(n)]#记录表记录已经计算过的节点,避免了重复的计算
    dp[0]=1
    dp[1]=1
    i=2
    while i < n:
        dp[i]=dp[i-1]+dp[i-2]
        i+=1
    return dp[n-1]

我利用记录表大幅度减少了重复计算的开销,并且我是从n=3开始从小到大依次计算结果,直接返回值。这就延伸出递归和动态规划的差别:

  1. 递归是‘从上到下’,递归就像是我们刚才画的树结构,从根节点不断分支,一直到触底,然后再逐层返回结果。
  2. 动态规划直‘从下到上’,动态规划是从最底层的问题开始计算,一直往上推,由循环迭代计算,得出我们想要的答案。

在上述动态规划的解法中: d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] dp[i]=dp[i-1]+dp[i-2] dp[i]=dp[i1]+dp[i2]即为状态转移方程。这是动态规划最为重要的东西,能写出动态转移方程,即可完成动态规划。

在上述斐波那契数列中,我们缺少了动态规划中一个比较重要的要素:最优子结构(最值问题),因为斐波那契数列中不存在求最值问题。

那我们来看一下这一题:
leetcode332 : 零钱问题.

题目描述如下:

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1

我们按照步骤,寻找求解这道题的思路:

  • 需要寻找最少组成金额的硬币数dp[i]
  • 我们通过比较 硬币数dp[i- coin]+1 和dp[i]中最小的硬币个数 d p [ i ] = m i n ( d p [ i ] , d p [ i − c o i n ] + 1 ) dp[i]=min(dp[i],dp[i-coin]+1) dp[i]=min(dp[i],dp[icoin]+1) 这里有个需要注意的地方,一般情况下的动态规划是从dp[1],dp[2],dp[3]一个一个往上推,但是在数硬币这里,当coin=[5,6],amount=11时,我们只要dp[11]=min(dp[5]+1,inf),不需要求解dp[10],求解的dp[i]与coin的面值有关系,如图:
def coinChange(coins, amount):
		#创建一个记录表
        dp=[float('inf') for i in range(amount+1)]
        dp[0]=0
        #这里的状态转移方程比较特殊只求coin面额间隙的dp[i]
        for c in coins:
            for i in range(c,amount+1):
                dp[i]=min(dp[i],dp[i-c]+1)
        #还是无穷,就返回-1
        return dp[amount] if dp[amount]!=float('inf') else -1

3.最优子结构与dp数组

这一部分,我们主要讲述最优子结构是什么?以及 d p [ ] dp[ ] dp[]数组应该如何遍历?

3.1 最优子结构

经过上面的例题,大家可能有点感觉,我要求组成11元的最小硬币数,我就要分别求10元、9元、6元的最小硬币数,是的,最优子结构可以理解为:可以从子问题的最优结果中推出更大规模的最优问题。

再比如青蛙上台阶的问题,上10节台阶,每次只能上1步或者2步,有多种走法,当我们求dp[10]时,只需要知道dp[9]和dp[8]即可,因为10节台阶,就是由9节上一节,以及8节上2节即可完成。

机器人寻路问题也类似!

3.2 dp[]的遍历方法

在动态规划中, d p [ i ] dp[i] dp[i]是存储某个状态的数组,但是dp数组的遍历方向有很多种方式:1. 正向遍历 2. 反向遍历 3. 斜着遍历

d p [ i ] dp[i] dp[i]的遍历方式和你对 d p [ i ] dp[i] dp[i]所设置的状态有关,所以你只需要记住几个关键问题:

  • 遍历的过程中,所有的状态必须是已经计算出来的
  • 遍历的终点,必须是存储结果的那个位置。

对于一维数组来说,大多数的问题都是从左到右遍历,但对于二维数组来说,如何遍历才能使所有数据计算完毕?这需要我们多加练习,才能深入了解,各种遍历方式的优缺点。

4. 参考文章

《大话数据结构》
《算法小抄》
《python数据结构》

以上是关于柳小葱的力扣之路——动态规划详解的主要内容,如果未能解决你的问题,请参考以下文章

力扣技巧之动态规划力扣322:零钱兑换C++

硬币问题-动态规划详解

经典动态规划:「换硬币」系列三道问题详解

322.零钱兑换(动态规划)

动态规划——详解leetcode518 零钱兑换 II

LeetCode 418 零钱兑换II[动态规划] HERODING的LeetCode之路