动态规划
通过leetcode上面的5道基础动态规划题目,讲解求解动态规划问题的思路。
定义
对于动态规划问题,通常需要做3件事情:
- 问题目标
- 状态的定义:\(opt[n]\)
- 状态转移方程:\(opt[n] = best\_of(opt[n-1], opt[n-2], ...)\)
最大子序和
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
子问题:
\(M(j)\): 以j结尾的最大子序和。
\(A[j]\): 数组的第\(j\)个元素。
\[M(j) = \max \{ M(j-1) + A[j], A[j]\}
\]
代码:
class Solution(object):
def maxSubArray(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
if len(nums) == 1: # 数组只有一个元素
return nums[0]
max_ret = nums[0] # 存储最大子串和
cur_max = last_max = nums[0]
for i in range(1, len(nums)):
cur_max = max(last_max + nums[i], nums[i])
if cur_max > max_ret:
max_ret = cur_max
last_max = cur_max
return max_ret
代码执行结果:
时间复杂度: \(O(n)\)
最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
- 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
- 你算法的时间复杂度应该为 \(O(n^2)\) 。
子问题:
\(L(j)\): 以j结尾的最长上升子序列。
\(A[j]\): 数组的第\(j\)个元素。
\[L(j) = \mathop{\rm{max:}}_{
i \lt j
\atop
A[i] \lt A[j]}
\{L(i)\} + 1
\]
代码:
class Solution(object):
def lengthOfLIS(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
nums_len = len(nums)
if nums_len <= 1:
return nums_len
mem = [1 for _ in range(nums_len)] # 初始化,存储每个元素结果对应的LIS的长度
for i in range(1, nums_len):
for j in range(0, i):
if nums[j] < nums[i]:
mem[i] = max(mem[i], mem[j]+1)
return max(mem)
代码执行结果:
时间复杂度: \(O(n^2)\)
零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1
。
示例1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例2:
输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。
子问题:
\(M(j)\): 总金额j所需要的最少硬币个数。
\[M(j) = \min_i \{M(j-v_i)\} + 1
\]
代码:
class Solution(object):
def coinChange(self, coins, amount):
"""
:type coins: List[int]
:type amount: int
:rtype: int
"""
if amount == 0:
return 0
if len(coins) == 0:
return -1
if len(coins) == 1 and coins[0] > amount:
return -1
mem = [-1 for i in range(amount + 1)] # 建立存储空间并初始化
mem[0] = 0
for i in range(1, amount + 1):
cur_min = amount + 1
for c in coins:
if c <= i: # 当钱币面值小于当前需要凑的金额时
cur_min = min(mem[i-c], cur_min)
mem[i] = cur_min + 1 if cur_min < amount+1 else amount+1
if mem[-1] == amount + 1:
return -1
else:
return mem[-1]
代码执行结果:
时间复杂度: \(O(n)\)
0-1背包问题
找不到这个问题,暂时跳过。
编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例1:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例2:
输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')
子问题:
\(T(i,j)\): 把\(A[1 \cdots i]\)转换成\(B[1 \cdots j]\)的最小代价。
\[M(j) = \min_i \{M(j-v_i)\} + 1
\]
参考一下那本书,里面的编辑距离公式,有点复杂,暂时跳过。
代码:
class Solution(object):
def coinChange(self, coins, amount):
"""
:type coins: List[int]
:type amount: int
:rtype: int
"""
if amount == 0:
return 0
if len(coins) == 0:
return -1
if len(coins) == 1 and coins[0] > amount:
return -1
mem = [-1 for i in range(amount + 1)] # 建立存储空间并初始化
mem[0] = 0
for i in range(1, amount + 1):
cur_min = amount + 1
for c in coins:
if c <= i: # 当钱币面值小于当前需要凑的金额时
cur_min = min(mem[i-c], cur_min)
mem[i] = cur_min + 1 if cur_min < amount+1 else amount+1
if mem[-1] == amount + 1:
return -1
else:
return mem[-1]
代码执行结果: