【498】动态规划 —— Dynamic Programming
参考1:如何理解动态规划?
参考2:B站视频比较详细
参考3:详解动态规划算法
一、动态规划总结【参考3】
动态规划是用来解决最优问题(也可以用来求方案数)的一种方法,或者说是一种思想。而解决问题的过程,需要经历多个阶段,每一阶段都可以看成一个子问题,每个子问题都对应着一组状态。
使用动态规划一般会经历四个步骤:
-
定义原问题和子问题。
子问题是和原问题相似但规模较小的问题。 -
定义状态。
这里的状态大家可以认为就是某个函数的自变量,根据状态中包含的参数个数的不同,我们在编程时设置的DP数组的维度就不同。每个状态中的参数通常都能对应DP数组中某个元素的下标,而DP数组的元素就是这个状态对应的子问题的求解结果。 -
寻找状态转移方程。
这一步往往是最难的,大家需要找到关于状态之间的某种转移关系,这个关系往往是一个递推式子,根据这个递推式我们才能一步一步计算出DP数组里面的元素。另外别忘了确定边界条件,也就是我们递推的初始条件。另外,如果一个问题能用动态规划方法求解,需要满足最优子结构和无后效性,我在讲解例题时回避掉了这一点,但这不代表它们并不重要,因为这块的严格证明非常困难,后续如果你有兴趣可以学习专门的算法课程。 -
编程实现。
如果前三步大家逻辑都理顺了,那么编程不是大的问题。我们无非就是先初始化一个DP数组,再结合边界条件计算DP数组中的初始值,最后再利用循环来对DP数组进行迭代。一般DP数组中最后那个元素就是我们要解决的原问题的答案。
二、动态规划中的常见概念【参考3】
这里我们还是以求解斐波那契数列来举例子,尽管它不算严格的动态规划:
-
子问题和原问题
原问题就是你要求解的这个问题本身,子问题是和原问题相似但规模较小的问题(原问题本身就是子问题的最复杂的情形,即子问题的特例)。
例如:要求F(10), 那么求出F(10)就是原问题,求出F(k)(K≤10)都是子问题。 -
状态
状态就是子问题中会变化的某个量,可以把状态看成我们要求解的问题的自变量。
例如:我们要求的F(10),那么这里的自变量10就是一个状态。 -
状态转移方程
能够表示状态之间转移关系的方程,一般利用关于状态的某个函数建立起来。类似于数列的递推公式。
例如:F(n)=F(n-1)+F(n-2),当n为>2的整数时;当n=1或2时,F(n)=1,这种最简单的初始条件一般称为边界条件,也被称为基本方程。 -
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)
代码:
1 2 3 4 5 6 7 8 9 10 | 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)
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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)
- 将每个金额的组合数分别计算出来
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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) |
早期内容!
动态规划与其说是一个算法,不如说是一个方法论,就是从开始,沿着状态方程(递推公式)一步步推导出所需要结果的过程,是一个结局问题的思路和方法。主要包括以下三个步骤:
- 建立状态转移方程
- 缓存并服用以往结果
- 按顺序从小往大算
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
2014-11-25 【154】C#打包程序成安装包