动态规划
动态规划问题的一般形式就是求最值,比如说让你求最长递增子序列呀,最小编辑距离呀等等。
重叠子问题、最优子结构、状态转移方程就是动态规划三要素:
「重叠子问题」 → 如果暴力穷举的话效率会极其低下,所以需要「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。
具备「最优子结构」,才能通过子问题的最值得到原问题的最值。
动态规划的核心思想就是穷举求最值,但只有列出正确的「状态转移方程」才能正确地穷举。
辅助思考: 明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义。
代码框架:
# 初始化 base case
dp[0][0][...] = base
# 进行状态转移
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 求最值(选择1,选择2...)
举例子: 凑零钱问题(leetcode#322)
题目:给你 k 种面值的硬币,面值分别为 c1, c2 ... ck,每种硬币的数量无限,再给一个总金额 amount,问你最少需要几枚硬币凑出这个金额,如果不可能凑出,算法返回 -1 。
解题步骤:
确定 base case,这个很简单,显然目标金额 amount 为 0 时算法返回 0,因为不需要任何硬币就已经凑出目标金额了。
确定「状态」,也就是原问题和子问题中会变化的变量。由于硬币数量无限,硬币的面额也是题目给定的,只有目标金额会不断地向 base case 靠近,所以唯一的「状态」就是目标金额 amount。
确定「选择」,也就是导致「状态」产生变化的行为。目标金额为什么变化呢,因为你在选择硬币,你每选择一枚硬币,就相当于减少了目标金额。所以说所有硬币的面值,就是你的「选择」。
明确 dp 函数/数组的定义。我们这里讲的是自顶向下的解法,所以会有一个递归的 dp 函数,一般来说函数的参数就是状态转移中会变化的量,也就是上面说到的「状态」;函数的返回值就是题目要求我们计算的量。就本题来说,状态只有一个,即「目标金额」,题目要求我们计算凑出目标金额所需的最少硬币数量。
所以我们可以这样定义 dp 函数:dp(n) 的定义 输入一个目标金额 n,返回凑出目标金额 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
# 题目要求的最终结果是 dp(amount)
return dp(amount)
但 总时间复杂度为 O(k * n^k),指数级别。
通过备忘录消除子问题:
def coinChange(coins: List[int], amount: int):
# 备忘录
memo = dict()
def dp(n):
# 查备忘录,避免重复计算
if n in memo: return memo[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)
# 记入备忘录
memo[n] = res if res != float('INF') else -1
return memo[n]
return dp(amount)
dp 数组的定义:当目标金额为 i 时,至少需要 dp[i] 枚硬币凑出。
int coinChange(vector& coins, int amount) {
// 数组大小为 amount + 1,初始值也为 amount + 1
vector dp(amount + 1, amount + 1);
// base case
dp[0] = 0;
// 外层 for 循环在遍历所有状态的所有取值
for (int i = 0; i < dp.size(); i++) {
// 内层 for 循环在求所有选择的最小值
for (int coin : coins) {
// 子问题无解,跳过
if (i - coin < 0) continue;
dp[i] = min(dp[i], 1 + dp[i - coin]);
}
}
return (dp[amount] == amount + 1) ? -1 : dp[amount];
}
动态规划是一种用于解决最优化问题的算法,它通过将大问题分解为子问题来求解。博客详细介绍了动态规划的三个关键要素:重叠子问题、最优子结构和状态转移方程,并通过凑零钱问题展示了如何应用动态规划。通过备忘录或DP表优化计算过程,避免重复计算,从而降低时间复杂度。最后,提供了动态规划问题的代码框架和伪码,帮助读者理解动态规划的实现思路。
332

被折叠的 条评论
为什么被折叠?



