【DP-04】动态规划算法题目解析

目录

  1. 121. 买卖股票的最佳时机/面试题63. 股票的最大利润
  2. 面试题19. 正则表达式匹配/10. 正则表达式匹配
  3. 剑指 Offer 60. n个骰子的点数
  4. 剑指 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 == 0return 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 == 0return 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的,而不是部分字符串。

说明:

可能为空,且只包含从 a-z 的小写字母。

可能为空,且只包含从 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] 的状态呢?我们可以分三种情况讨论,其中,前两种情况考虑了所有能匹配的情况,剩下的就是不能匹配的情况了:

  1. 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 加一个 . ,仍然是匹配的。
  2. 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]
  1. 其他情况:以上两种情况把能匹配的都考虑全面了,所以其他情况为不匹配,即 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) == 1return False

        m,n = len(s)+1len(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[0in {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个骰子可能出现16的点数。所以第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

解释: 122585种不同的翻译,分别是"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]

 

参考文章:

1https://leetcode-cn.com/

原文地址:https://www.cnblogs.com/yifanrensheng/p/13510817.html