文章目录
动态规划
动态规划(Dynamic Programming,简称DP)是一种用于解决优化问题的算法思想。它将复杂问题分解为简单子问题,并存储这些子问题的解,以避免重复计算。动态规划通常用于解决具有重叠子问题和最优子结构特性的问题。
以下是动态规划的一些核心概念:
核心思想
- 分治:将原问题分解为更小的子问题。
- 存储子问题的解:将子问题的解存储起来,以避免重复计算。
- 构建原问题的解:利用子问题的解构建原问题的解。
关键特性
- 重叠子问题:原问题可以被分解为若干个子问题,而这些子问题不是独立的,即它们会重复出现。
- 最优子结构:一个问题的最优解包含其子问题的最优解。
动态规划的步骤
- 定义状态:确定状态变量,状态变量通常是问题的参数,它们的变化会导致问题的解发生变化。
- 状态转移方程:找出状态变量之间的关系,即如何从一个或多个已知状态的解得到另一个状态的解。
- 边界条件:确定状态变量的初始值或边界情况。
动态规划的两种实现方式
- 自顶向下(Top-Down):从最终目标开始递归求解,在递归过程中存储和复用子问题的解。
- 自底向上(Bottom-Up):从最简单的子问题开始,逐步迭代求解,直到得到最终问题的解。
示例:斐波那契数列
斐波那契数列是一个经典的动态规划问题。其递推关系为:
F(n) = F(n-1) + F(n-2)
边界条件为:
F(0) = 0, F(1) = 1
以下是使用动态规划求解斐波那契数列的Python代码:
def fibonacci(n):
if n <= 1:
return n
dp = [0] * (n+1)
dp[1] = 1
for i in range(2, n+1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
# 测试
print(fibonacci(5)) # 输出应为5
在这个例子中,dp
数组用于存储子问题的解,从而避免重复计算。
总结
动态规划是一种强大的算法工具,它通过将问题分解为重叠的子问题,并存储子问题的解来优化计算过程。掌握动态规划的关键在于识别问题是否具有重叠子问题和最优子结构特性,以及如何定义状态和状态转移方程。
例题1.跳青蛙、爬楼梯
当然,让我们通过青蛙跳台阶的例子来详细解释动态规划的概念和步骤。
leetcode上第 70题: 爬楼梯
https://leetcode.cn/problems/climbing-stairs/
问题描述
假设一只青蛙要跳上n级台阶,每次它可以选择跳上1级或者2级台阶。问:这只青蛙有多少种不同的跳法?
动态规划步骤
1. 定义状态
在这个问题中,我们可以定义dp[i]
为跳上第i
级台阶的不同跳法数量。
2. 状态转移方程
根据问题描述,青蛙到达第i
级台阶可以从第i-1
级台阶跳上来,或者从第i-2
级台阶跳上来。因此,状态转移方程为:
dp[i] = dp[i-1] + dp[i-2]
3. 边界条件
- 当
i=1
时,只有一种跳法,即直接跳上第一级台阶,所以dp[1] = 1
。 - 当
i=2
时,有两种跳法,一是从第一级跳上来,二是直接跳上两级台阶,所以dp[2] = 2
。
4. 计算顺序
我们从最小的子问题开始,逐步计算到原问题。即先计算dp[1]
和dp[2]
,然后使用它们来计算dp[3]
,以此类推,直到计算到dp[n]
。
动态规划实现
以下是使用动态规划求解青蛙跳台阶问题的Python代码:
def frogJump(n):
# 边界条件处理
if n == 1:
return 1
if n == 2:
return 2
# 初始化dp数组
dp = [0] * (n+1)
dp[1] = 1
dp[2] = 2
# 计算dp[i],i从3到n
for i in range(3, n+1):
dp[i] = dp[i-1] + dp[i-2]
# 返回结果
return dp[n]
# 测试
print(frogJump(5)) # 输出应为8
在这个代码中,dp
数组用于存储每个子问题的解,这样我们就不需要重复计算它们。
解释
- 我们首先处理了边界条件,即当台阶只有1级或2级时的情况。
- 然后我们初始化了一个长度为
n+1
的数组dp
,用于存储从0级到n级台阶的跳法数量。 - 接下来,我们使用一个循环从3级台阶开始计算到n级台阶的跳法数量。每次计算
dp[i]
时,我们只需查看dp[i-1]
和dp[i-2]
的值,并将它们相加即可。 - 最后,
dp[n]
就是我们的答案,即跳上n级台阶的不同跳法数量。
通过这个例子,我们可以看到动态规划是如何通过分解问题、存储子问题解以及逐步构建最终解来高效解决问题的。
例题2.零钱兑换
https://leetcode.cn/problems/coin-change/
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
硬币找零问题要求计算使用给定硬币面额列表中的硬币组合成指定金额所需的最少硬币数量。
代码的实现步骤如下:
- 初始化动态规划数组:创建一个长度为
amount + 1
的数组dp
,并将其所有元素初始化为无穷大,表示不可能用这些硬币组合出这个金额。dp[0]
被初始化为0,因为0元找零不需要任何硬币。 - 填充动态规划数组:遍历金额
i
从1到amount
,对于每个金额,遍历所有可能的硬币面额coin
。如果当前金额i
大于或等于硬币面额coin
,则更新dp[i]
为当前金额所需硬币数的最小值,即dp[i - coin] + 1
(因为使用了coin
面额的硬币,所以数量加1)。 - 返回结果:最后,返回
dp[amount]
的值。如果dp[amount]
不是无穷大,则表示存在至少一种方法使用给定的硬币组合成指定金额;否则,返回-1,表示没有足够的硬币组合成指定金额。
这种动态规划方法的优点是它只需要一次遍历,因此其时间复杂度为O(amount * n),其中n是硬币面额的数量。空间复杂度为O(amount),因为需要一个大小为amount + 1
的数组来存储状态。
请注意,这段代码假设所有硬币都是正数,且amount
是小于或等于所有硬币面额的最大值。如果amount
大于所有硬币面额的最大值,dp[amount]
将被初始化为无穷大,最终返回-1。
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
# 维护一张dp表,表示从0到amount最少几次找零
# dp = [float('inf')]*(amount+1)
# 这是float('inf')表示正无穷大,float('-inf')表示负无穷大,不理解可以用9999999代替
dp = [1999999]*(amount+1)
dp[0]=0
if amount==0:
return 0
for i in range(1,amount+1):
for coin in coins:
if i>=coin:
dp[i] = min(dp[i],dp[i-coin]+1)
# if dp[amount] == float('inf'):
if dp[amount] == 1999999:
return -1
else:
return dp[amount]