前言
动态规划问题一直是算法学习中的一个难点问题,但也是一个重点问题。在这次的面试中也遇到了动态规划的相关问题。
本文是对大神labuladong的一篇关于动态规划文章的个人摘要,原文请移步动态规划详解(修订版)。
动态规划三要素
动态规划问题的本质其实就是通过穷举得到一个问题的最优解,只是能够通过动态规划解决的问题都会满足下面三个要素。
重叠子问题
如果单纯使用暴力穷举的话,效率会非常低下,所以动态规划问题常常会用备忘录或者dp table保存计算的中间状态结果。
最优子结构
动态规划问题必须满足最优子结构,这样才能通过子问题的最优解得到目标问题的最优解。
状态转移方程
如何从一个子问题的最优解计算出下一个子问题的最优解,从而最终计算出目标问题的最优解,这中间的计算公式就是我们要找出的状态转移方程。
找出状态转移方程是动态规划问题中最难的一步,下面提供一个找出动态转移方程的思维框架:
明确状态–>明确dp数组/函数的意义–>明确选择–>明确base case
凑零钱问题说明如何找到状态转移方程
给你k种面值的硬币,面值分别为c1, c2 …
ck,每种硬币的数量无限,再给一个总金额amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。
先确定状态,也就是原问题和子问题中变化的变量,由于硬币数量无限,所以唯一变化的量就是金额amount。
再确定dp函数的定义,函数dp(n)表示,当前的目标金额是n,至少需要dp(n)个硬币凑出金额。
然后确定选择并择优,*也就是对于每个状态,可以做出什么选择改变当前状态。*具体到这个问题,无论当前的目标金额是多少,选择就是从面额列表中选择一个硬币,然后目标金额就会减少。
伪代码框架
def coinChange(coins:List[int],amount:int)
//定义:要凑出金额n,至少需要dp(n)个硬币
def dp(n):
//做选择,需要硬币最少的那个结果就是答案
for coin in coins:
res = min(res, 1 + dp(n-coin))
return res
//目标金额是amount
return dp(amount);
最后明确base case,显然目标金额是0时,需要的硬币个数为0;当目标金额小于0时,无解,返回-1:
def coinChange(coins:List[int],amount:int)
//定义:要凑出金额n,至少需要dp(n)个硬币
def dp(n):
// base case
if n == 0: return 0
if n < 0: return -1
//求最小解,所以初始化为正无穷
res = float('INF')
//做选择,需要硬币最少的那个结果就是答案
for coin in coins:
subproblem = dp(n - coin)
//子问题无解,跳过
if subproblem == -1:continue
res = min(res, 1 + subproblem )
return res if res != float('INF') else -1
//目标金额是amount
return dp(amount);
用数学形式表示状态转移方程(是不是很像数学归纳法的写法):
至此,这个问题已经解决了,只是要解决下重叠子问题。解决重叠子问题的方法,
- 一是通过备忘录的方式,对于子问题的结果优先从备忘录中获取
- 二是用dp table,自底向上的计算子问题
int coinChange(vector<int> &coins, int amount)
{
//初始化dp table
//数组大小为amount+1,初始值也为amount+1
vector<int> dp(amount+1,amount+1);
//base case
dp[0] = 0;
for (int i = 0; i < dp.size(); i++)
{
for (int coin : coins)
{
//子问题无解,跳过
if (i - coin < 0) continue;
dp[i] = min(dp[i - coin] + 1,dp[i]);
}
}
return (dp[amount] == amount + 1) ? -1 : dp[amount];
}
动态规划经典问题解析
0-1背包问题
有一个容量为M的背包,有N个物品,物品的重量为wgt[],物品的价值为val[],如何装物品可以获得最大价值
首先确定这是一个动态规划的问题,满足动态规划的三个因素。然后按照需要列出动态转移方程,套用上面的四大步骤:
- 明确状态
本题的状态是背包的容量和可选的物品还剩多少 - 确定动态函数/dp数组的含义
dp数组的维度通常是根据状态的维度来确定的,比如dp[i][w]的含义是对于前i个物品,当背包容量为w时,可以装下的最大价值是dp[i][w]。其实这就是一个中间状态,状态转移的起始点。同时也可以得出base case是dp[0][w] = 0,dp[i][0] = 0。 - 确定选择和择优
背包问题的选择就是放入i个物品或者不放入i个物品。择优是在选择之后,背包容量还有剩余,而且价值大于当前价值。