什么是动态规划
动态规划(Dynamic Programming,简称DP),如果某一问题有很多重叠子问题,使用动态规划是最有效的。所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。
动态规划的解题步骤
五步曲:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式(根据dp数组的定义,运用数学归纳法的思想,假设dp[0...i-1]都已知,想办法求出dp[i],一旦这一步完成,整个题目基本就解决了。但如果无法完成这一步,很可能就是dp数组的定义不够恰当,需要重新定义dp数组的含义;或者可能是dp数组存储的信息还不够,不足以推出下一步的答案,需要把dp数组扩大成二维数组甚至三维数组)
- dp数组如何初始化
- 确定遍历顺序
- 举例推导、打印dp数组
爬楼梯问题
LeetCode746题 使用最小花费爬楼梯
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
# dp[i]表示跳到i的最小花费,只需记录最近两个状态
# 由于跳到前两个位置不需要花费,所以初始化为0
dp = [0, 0]
for i in range(2, len(cost)):
tmp = min(dp[0] + cost[i - 2], dp[1] + cost[i - 1])
dp[0] = dp[1]
dp[1] = tmp
return min(dp[0] + cost[len(cost) - 2], dp[1] + cost[len(cost) - 1])
LeetCode91题 解码方法
class Solution:
def numDecodings(self, s: str) -> int:
if s[0] == '0':
return 0
# dp[i]代表s[:i+1]的解码方法数,只需记录最近两个状态
dp = [1, 1]
for i in range(1, len(s)):
# 无法解码
if s[i] == '0' and s[i - 1] not in {'1', '2'}:
return 0
# s[i]只能和s[i-1]一起解码
elif s[i] == '0' and s[i - 1] in {'1', '2'}:
r = dp[0]
# s[i]只能独自解码
elif s[i - 1] == '0' or int(s[i - 1:i + 1]) > 26:
r = dp[1]
# 其他有两种解码方式情况
else:
r = dp[0] + dp[1]
dp[0], dp[1] = dp[1], r
return dp[-1]
打家劫舍问题
LeetCode198题 打家劫舍
class Solution:
def rob(self, nums: List[int]) -> int:
# 二维数组解法
# dp = [[0] * 2 for _ in range(len(nums))]
# 一维数组解法
dp = [0] * 2
for i in range(len(nums)):
# dp[i][0] = max(dp[i - 1][0], dp[i - 1][1])
# dp[i][1] = dp[i - 1][0] + nums[i]
dp[0], dp[1] = max(dp[0], dp[1]), dp[0] + nums[i]
return max(dp[0], dp[1])
LeetCode3186题 施咒的最大总伤害
class Solution:
def maximumTotalDamage(self, power: List[int]) -> int:
# 重构power数组,使之转化为打家劫舍问题
power.sort()
power_ = []
power_sum = []
for i in range(len(power)):
if i > 0 and power[i] == power[i - 1]:
power_sum[-1] += power[i]
else:
power_.append(power[i])
power_sum.append(power[i])
# 打家劫舍问题求解
dp_pre = [0, 0]
dp = [0, 0]
for i in range(len(power_)):
dp_copy = dp[:]
if i >= 2 and power_[i] - power_[i - 2] == 2:
dp[0], dp[1] = max(dp[0], dp[1]), dp_pre[0] + power_sum[i]
elif i >= 1 and power_[i] - power_[i - 1] <= 2:
dp[0], dp[1] = max(dp[0], dp[1]), dp[0] + power_sum[i]
else:
dp[0], dp[1] = max(dp[0], dp[1]), max(dp[0], dp[1]) + power_sum[i]
dp_pre = dp_copy
return max(dp[0], dp[1])
LeetCode213题 打家劫舍II
将环形问题转化为线性问题,分别计算不考虑首尾元素的情况
class Solution:
def rob(self, nums: List[int]) -> int:
if len(nums) == 1:
return nums[0]
dp_1 = [0] * 2
# 不考虑尾元素
for i in range(len(nums) - 1):
dp_1[0], dp_1[1] = max(dp_1[0], dp_1[1]), dp_1[0] + nums[i]
dp_2 = [0] * 2
# 不考虑首元素
for i in range(1, len(nums)):
dp_2[0], dp_2[1] = max(dp_2[0], dp_2[1]), dp_2[0] + nums[i]
return max(dp_1[0], dp_1[1], dp_2[0], dp_2[1])
LeetCode337题 打家劫舍III
树形DP解法,遍历二叉树的递归函数返回值为DP数组
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def rob(self, root: Optional[TreeNode]) -> int:
res = self.traversal(root)
return max(res[0], res[1])
def traversal(self, root):
# dp数组含义:
# 索引为0记录不偷该节点所得到的的最大金钱
# 索引为1记录偷该节点所得到的的最大金钱
if root is None:
return [0, 0]
left = self.traversal(root.left)
right = self.traversal(root.right)
return [max(left[0], left[1]) + max(right[0], right[1]), left[0] + right[0] + root.val]
其他典型问题
LeetCode343题 整数拆分
class Solution:
def integerBreak(self, n: int) -> int:
# dp[i]代表拆i的最大乘积
dp = [1] * (n + 1)
for i in range(3, n + 1):
# 拆分为2个元素最大乘积
tmp = i // 2 * (i - i // 2)
# 搜索拆分为大于2个元素的最大乘积
for j in range(1, i - 1):
tmp = max(tmp, j * dp[i - j])
dp[i] = tmp
return dp[n]
难在dp数组定义、递推公式的问题
LeetCode1696题 跳跃游戏VI
动态规划 + 单调队列解法:可以将dp数组设置为一个单调队列,只保留之前k个位置有机会成为最大的分数,这样队首元素就代表着前k个位置的最大分数。降低时间复杂度O(k*n)->O(n)
class Solution:
def maxResult(self, nums: List[int], k: int) -> int:
from collections import deque
# 维护一个单调队列,降低时间复杂度O(k*n)->O(n)
queue = deque([nums[0]])
for i in range(1, len(nums)):
# 此时nums[i]代表到达索引i的最大得分
nums[i] = queue[0] + nums[i]
# 到达第k步时,开始弹出符合条件的元素
if i - k >= 0 and queue[0] == nums[i - k]:
queue.popleft()
# 将nums[i]加入单调队列
while queue and queue[-1] < nums[i]:
queue.pop()
queue.append(nums[i])
return nums[-1]
LeetCode221题 最大正方形
dp[i][j]定义为以(i, j)为右下角的最大正方形边长
递推公式:dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1
class Solution:
def maximalSquare(self, matrix: List[List[str]]) -> int:
# dp[i][j]代表以(i, j)为右下角的最大正方形边长
# dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1
res = 0
m, n = len(matrix), len(matrix[0])
dp = [[0] * n for _ in range(m)]
for i in range(m):
for j in range(n):
if matrix[i][j] == '0':
continue
if i == 0 or j == 0:
dp[i][j] = 1
else:
dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1
res = max(res, dp[i][j])
return res * res
LeetCode787题 K站中转内最便宜的航班
class Solution:
def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int:
# dp[t][i]代表从src出发,t次中转到i的最小花费
# dp[t][i] = 对可到达i的j 取min{dp[t−1][j] + price(j,i)}
# dp数组初始化
dp = [float('inf')] * n
dp[src] = 0
res = dp[dst]
for t in range(k + 1):
tmp_dp = [float('inf')] * n
for s, d, p in flights:
tmp_dp[d] = min(tmp_dp[d], dp[s] + p)
res = min(res, tmp_dp[dst])
dp = tmp_dp
if res == float('inf'):
return -1
return res