动态规划(DP)是一种通过将问题分解为子问题,并通过保存子问题的结果,避免重复计算,最终解决复杂问题的优化方法。其核心思想就是通过存储子问题的解,从而避免重复计算。
动态规划的基本步骤
-
定义状态:
- 确定动态规划的状态是什么,通常通过一个数组(或表格)
dp[]
来表示问题的不同子问题的解。
- 确定动态规划的状态是什么,通常通过一个数组(或表格)
-
状态转移方程:
- 确定状态之间的关系,即如何通过已知的子问题解推导出当前问题的解。这就是状态转移方程,是动态规划的核心。
-
初始化:
- 对于某些基础状态(通常是最小规模的问题),需要进行初始化。
-
计算顺序:
- 通常,动态规划是从最小的子问题开始,逐步计算更大的子问题的解。
-
返回结果:
- 最终问题的解通常存储在数组的最后一个位置,或者是某些特定的子问题状态。
我们通过一些题目,逐渐深入的讲解。
例1
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
。
你可以认为每种硬币的数量是无限的。
我们按照上面动态规划基本步骤的思路来解决这个题。
步骤1:定义状态
我们需要定义一个数组 dp
来存储每个金额所需的最少硬币数。
- 状态:
dp[i]
表示凑成金额i
所需的最少硬币数。dp[0] = 0
,表示凑成金额 0 所需的硬币数是 0。dp[i]
的值应为从dp[i - coin] + 1
(其中coin
是硬币的面值)中挑选出最小的值。
步骤2:状态转移方程
我们需要通过所有硬币来更新每个 dp[i]
的值,状态转移方程为:
dp[i]=min(dp[i],dp[i−coin]+1)dp[i] = \min(dp[i], dp[i - coin] + 1)dp[i]=min(dp[i],dp[i−coin]+1)
其中 i
是当前目标金额,coin
是硬币的面值。如果当前的金额 i
大于等于 coin
,我们就可以通过 dp[i - coin]
加 1 来更新 dp[i]
。
步骤3:初始化
dp[0] = 0
,即凑成 0 元需要 0 个硬币。- 对于其他金额
i
,我们初始化为一个很大的值(例如float('inf')
),表示当前金额无法凑成。
步骤4:计算顺序
我们从金额 1
到 amount
依次计算每个 dp[i]
,对于每个 i
,我们遍历所有的硬币面值 coin
,依次计算最小的硬币数。
步骤5:返回结果
最后我们检查 dp[amount]
是否仍然为初始值 float('inf')
,如果是,说明无法凑成该金额,返回 -1
;否则返回 dp[amount]
,即为最少的硬币数。
代码实现
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
# 初始化 dp 数组,初始值为正无穷,表示无法凑成
dp = [float('inf')] * (amount + 1)
dp[0] = 0 # 金额为 0 时,所需硬币数为 0
# 动态规划,计算每个金额的最小硬币数
for i in range(1, amount + 1):
for coin in coins:
if i >= coin:
dp[i] = min(dp[i], dp[i - coin] + 1)
# 如果 dp[amount] 仍然为无穷大,表示无法凑成,返回 -1
return dp[amount] if dp[amount] != float('inf') else -1
例2
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
解题思路:典型的动态规划题,n阶台阶,第一步只能走1步或2步,剩下则为n-1阶或n-2阶楼梯,典型的斐波那契数列。n阶台阶的方法=n-1阶台阶的方法+n-2阶台阶的方法
代码实现
class Solution:
def climbStairs(self, n: int) -> int:
if n==1:
return 1
dp=[0]*(n+1)
dp[1]=1
dp[2]=2
for i in range(3,n+1):
dp[i]=dp[i-1]+dp[i-2]
return dp[n]
这段代码用了 O(n)
的空间来存储 dp
数组,但实际上我们只需要存储前两个状态 dp[i-1]
和 dp[i-2]
,因此可以将空间复杂度优化为 O(1)
。
更优的解法
class Solution:
def climbStairs(self, n: int) -> int:
if n==1:
return 1
elif n==2:
return 2
prev1,prev2=2,1
for i in range(3,n+1):
curr=prev1+prev2
prev2=prev1
prev1=curr
return curr