柳小葱的力扣之路——动态规划详解
Posted 柳小葱
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了柳小葱的力扣之路——动态规划详解相关的知识,希望对你有一定的参考价值。
🌰今天写的这篇动态规划,算是对上一篇的补充,在上一篇的动态规划中,我们主要讲述了动态规划的基本概念和在一些简单题的应用,在这一期中,我们将详细讲述,动态规划适合求解那一类问题?和递归有啥不同?求解动态规划问题的套路是什么?对上一期内容感兴趣的小伙伴可以查看下面👇:
- 链接: python数据结构之动态规划.
💝我们主要将从套路出手,解决一一些算法题中常见的动态规划问题。希望看完之后,让给你对动态规划有更深的理解。
1. 动态规划的套路
动态规划的求解的套路主要有下面三个步骤:
- 找到问题的状态 i
- 明确dp[i]数组的含义
- 寻找状态i ,i-1,…之间的关系
碰见动态规划问题,最好从以上三个部分下手,因为这样子能够很好的抓住动态规划的本质。
还需要注意的是,动态规划求解方法主要分为以下3种:
- 暴力的递归解法
- 带记录的递归解法
- 迭代的动态规划求解法
2. 动态规划详解
在算法题中,动态规划问题的一般形式就是求最值。比如说让你求最长递增子序列呀,最小编辑距离呀等等。解决动态规划的核心问题是穷举,我们穷举出所有的答案,再在其中寻找最值即可,但穷举的时候需要注意下面几个问题:
- 问题存在大量重复的计算,暴力穷举效率低下
- 具备最优子结构
- 列出正确的状态转移方程
在这些问题中如何写出正确的状态转移方程是最需要注意的问题。
斐波那契数列指的是这样一个数列:
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开始从小到大依次计算结果,直接返回值。这就延伸出递归和动态规划的差别:
- 递归是‘从上到下’,递归就像是我们刚才画的树结构,从根节点不断分支,一直到触底,然后再逐层返回结果。
- 动态规划直‘从下到上’,动态规划是从最底层的问题开始计算,一直往上推,由循环迭代计算,得出我们想要的答案。
在上述动态规划的解法中: d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] dp[i]=dp[i-1]+dp[i-2] dp[i]=dp[i−1]+dp[i−2]即为状态转移方程。这是动态规划最为重要的东西,能写出动态转移方程,即可完成动态规划。
在上述斐波那契数列中,我们缺少了动态规划中一个比较重要的要素:最优子结构(最值问题),因为斐波那契数列中不存在求最值问题。
那我们来看一下这一题:
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[i−coin]+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数据结构》
以上是关于柳小葱的力扣之路——动态规划详解的主要内容,如果未能解决你的问题,请参考以下文章