【DP-04】动态规划算法题目解析
目录
- 121. 买卖股票的最佳时机/面试题63. 股票的最大利润
- 面试题19. 正则表达式匹配/10. 正则表达式匹配
- 剑指 Offer 60. n个骰子的点数
- 剑指 Offer 46. 把数字翻译成字符串
一、121. 买卖股票的最佳时机/面试题63. 股票的最大利润
1.1 问题:
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
注意:你不能在买入股票前卖出股票。
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
1.2 求解:
1)步骤一:定义子问题
和明显,使用dp[i],i表示前i天的最大化,i从取n表示原始问题,i取k表示子问题,子问题比较容易扩展成原始问题。
2)写出子问题的递推关系
初始值:dp = [0] * n ,边界情况 if n == 0: return 0
3)确定 DP 数组的计算顺序
启发式的计算顺序,代码见1.3代码一
4 )空间优化(可选)
因为只用到了dp[i-1],则可以将其空间压缩,将dp[i-1]更换成max_temp
1.3 代码
代码一:
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
if n == 0: return 0
dp = [0]*n
minprice = prices[0]
for i in range(n):
minprice = min(minprice,prices[i])
dp[i] = max(dp[i-1],prices[i]-minprice)
return dp[-1]
代码二:
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
if n == 0: return 0
max_temp = 0
minprice = prices[0]
for i in range(n):
minprice = min(minprice,prices[i])
max_temp = max(max_temp,prices[i]-minprice)
return max_temp
二、面试题19. 正则表达式匹配/10. 正则表达式匹配
2.1 问题:
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。
'.' 匹配任意单个字符
'*' 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 *。
示例 1:
输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:
s = "aa"
p = "a*"
输出: true
解释: 因为 '*' 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 'a'。因此,字符串 "aa" 可被视为 'a' 重复了一次。
示例 3:
输入:
s = "ab"
p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。
示例 4:
输入:
s = "aab"
p = "c*a*b"
输出: true
解释: 因为 '*' 表示零个或多个,这里 'c' 为 0 个, 'a' 被重复一次。因此可以匹配字符串 "aab"。
示例 5:
输入:
s = "mississippi"
p = "mis*is*p*."
输出: false
2.2 求解:
1)步骤一:定义子问题
可以看着二维矩阵的形式,前面[i][j]规范好了,后面增加部分只要能匹配就可以,最终得到[m][n]最终情况。
2)写出子问题的递推关系
转移方程
现在如果已知了 dp[i-1][j-1] 的状态,我们该如何确定 dp[i][j] 的状态呢?我们可以分三种情况讨论,其中,前两种情况考虑了所有能匹配的情况,剩下的就是不能匹配的情况了:
- s[i] == p[j] or p[j] == '.':比如 abb 和 abb,或者 abb 和 ab. ,很容易得到 dp[i][j] = dp[i-1][j-1] = True。因为 ab 和 ab 是匹配的,如果后面分别加一个 b,或者 s 加一个 b 而 p 加一个 . ,仍然是匹配的。
- p[j] == '*':当 p[j] 为星号时,由于星号与前面的字符相关,因此我们比较星号前面的字符 p[j-1](根据星号前面的字符) 和 s[i] 是否相等,分两种情况:
- 相等:p[j-1] == s[i] or p[j-1] == '.':星号前面的字符可以与 s[i] 匹配,这种情况下,星号可能匹配了前面的字符的 0 个,也可能匹配了前面字符的多个,当匹配 0 个时,如 ab 和 abb*,或者 ab 和 ab.* ,这时我们需要去掉 p 中的 b* 或 .* 后进行比较,即 dp[i][j] = dp[i][j-2];当匹配多个时,如 abbb 和 ab*,或者 abbb 和 a.*,我们需要将 s[i] 前面的与 p 重新比较,即 dp[i][j] = dp[i-1][j]
- 不相等:p[j-1] != s[i]:如果星号前一个字符匹配不上,星号匹配了 0 次,应忽略这两个字符,看 p[j-2] 和 s[i] 是否匹配。 这时 dp[i][j] = dp[i][j-2]。
- 其他情况:以上两种情况把能匹配的都考虑全面了,所以其他情况为不匹配,即 dp[i][j] = False
将以上进行归纳得到状态转移方程:
初始化:初始化定义第一行的值(若出现星号,可以考虑删除,退化到找前推2个(dp[0][c] = dp[0][c - 2]))
dp = [[False for _ in range(n)] for _ in range(m)]
# 初始状态
dp[0][0] = True
dp[0][1] = False
for c in range(2, n):
j = c - 1
if p[j] == '*':
dp[0][c] = dp[0][c - 2]
3)确定 DP 数组的计算顺序
启发式形式,定义好第一行,从上到下进行处理。具体代码见2.3代码一
4 )空间优化(可选)
问题较难,没有优化了。
2.3 代码
代码一:
class Solution:
def isMatch(self, s: str, p: str) -> bool:
# 边界条件,考虑 s 或 p 分别为空的特殊情况情况
if not p: return not s
if not s and len(p) == 1: return False
m,n = len(s)+1, len(p)+1
dp = [[False for _ in range(n)] for _ in range(m)] #(m+1)(n+1)的矩阵
#initializtion
dp[0][0] = True
dp[0][1] = False
for c in range(2,n):
j = c -1
if p[j] == '*' :
dp[0][c] = dp[0][c-2]
for r in range(1,m):
i = r -1 #比较的是前置项,而且出现次数较多,故单独令下
for c in range(1,n):
j = c -1
if s[i] == p[j] or p[j] == ".":
dp[r][c] = dp[r-1][c-1]
elif p[j] == '*' :
if p[j-1] == s[i] or p[j-1] ==".":
dp[r][c] = dp[r-1][c] or dp[r][c-2] #注意这里有分了两种情况,第二个的举例:ab 和 ab.*
else:dp[r][c] = dp[r][c-2]
else:dp[r][c] == False
return dp[-1][-1]
代码二:回溯法
class Solution:
def isMatch(self, s: str, p: str) -> bool:
if not p: return not s
# 第一个字母是否匹配
first_match = bool(s and p[0] in {s[0],'.'})
# 如果 p 第二个字母是 *
if len(p) >= 2 and p[1] == "*":
return self.isMatch(s, p[2:]) or first_match and self.isMatch(s[1:], p)
else:
return first_match and self.isMatch(s[1:], p[1:])
三、剑指 Offer 60. n个骰子的点数
3.1 问题:
把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。
示例 1:
输入: 1
输出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]
示例 2:
输入: 2
输出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]
3.2 求解:
1)步骤一:定义子问题
子问题是和原问题相似,但规模较小的问题,可以利用小规模子问题刻画其结构特征。
说明:创建一个二维数组,保存每一个子问题的结果,具体创建一维数组还是二维数组看题目而定
注意:子问题的解一旦求出就会被保存,所以每个子问题只需求 解一次。
2)写出子问题的递推关系
总体思路:先求n个骰子点数的和为s的排列情况总数(见下),再除以6^n得到各自的概率。
- 确定问题解的表达式。可将f(n, s) 表示n个骰子点数的和为s的排列情况总数
- 确定状态转移方程。n个骰子点数和为s的种类数只与n-1个骰子的和有关。因为一个骰子有六个点数,那么第n个骰子可能出现1到6的点数。所以第n个骰子点数为1的话,f(n,s)=f(n-1,s-1),当第n个骰子点数为2的话,f(n,s)=f(n-1,s-2),…,依次类推。在n-1个骰子的基础上,再增加一个骰子出现点数和为s的结果只有这6种情况!那么有:f(n,s)=f(n-1,s-1)+f(n-1,s-2)+f(n-1,s-3)+f(n-1,s-4)+f(n-1,s-5)+f(n-1,s-6)
- 上面就是状态转移方程,已知初始阶段的解为:当n=1时, f(1,1)=f(1,2)=f(1,3)=f(1,4)=f(1,5)=f(1,6)=1。
3)确定 DP 数组的计算顺序
从自底向上的 dp 数组
4 )空间优化(可选)
3.3 代码
代码一:
class Solution:
def twoSum(self, n: int) -> List[float]:
dp = [ [0 for _ in range(6*n+1)] for _ in range(n+1)]
for i in range(1,7): #初始化
dp[1][i] = 1
for i in range(2,n+1): #次数循环,从2开始。从上到下的遍历
for j in range(i,i*6+1): #从第 i 开始进行计数,一直到 6*I,表示可能出现的和的情况;从做到右的遍历
for k in range(1,7): #状态转移方程
dp[i][j] +=dp[i-1][j-k]
res = []
for i in range(n,n*6+1): #回到问题,计算概率
res.append(dp[n][i]*1.0/6**n)
return res
四、剑指 Offer 46. 把数字翻译成字符串
4.1 问题
给定一个数字,我们按照如下规则把它翻译为字符串:0 翻译成 "a" ,1 翻译成 "b",……,11 翻译成 "l",……,25 翻译成 "z"。一个数字可能有多个翻译。请编程实现一个函数,用来计算一个数字有多少种不同的翻译方法。
示例 1:
输入: 12258
输出: 5
解释: 12258有5种不同的翻译,分别是"bccfi", "bwfi", "bczi", "mcfi"和"mzi"
4.2 求解:
1)步骤一:定义子问题
去掉一个对整体没有问题结构不造成影响,可以求出前面的情况,依次得出最终的。
2)写出子问题的递推关系
我们有两种选择:
只翻译自己;
和前面的数字组合翻译,前提是组合的数在 10-25之间。
3)确定 DP 数组的计算顺序
从底向上形式
4 )空间优化(可选)
不考虑
4.3 代码
class Solution:
def translateNum(self, num: int) -> int:
str_num = str(num)
n = len(str_num)
dp = [1 for _ in range(n + 1)] # n+1
for i in range(2, n + 1):#注意n >= 2,
if str_num[i - 2] == '1' or (str_num[i - 2] == '2' and str_num[i - 1] < '6'):
dp[i] = dp[i - 2] + dp[i - 1]
else:
dp[i] = dp[i - 1]
return dp[n]