动态规划专题

目录

1、动态规划要素

2、模板

3、实例

10. 正则表达式匹配

1143. 最长公共子序列

120. 三角形最小路径和

53. 最大连续子序和

63. 不同路径 II

72. 编辑距离

91. 解码方法

198. 打家劫舍

337. 打家劫舍 III

300. 最长上升子序列

312. 戳气球

518. 零钱兑换 II

664. 奇怪的打印机


1、动态规划要素

       动态规划的三要素:最优子结构,边界和状态转移函数,最优子结构是指每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到(子问题的最优解能够决定这个问题的最优解),边界指的是问题最小子集的解(初始范围),状态转移函数是指从一个阶段向另一个阶段过度的具体形式,描述的是两个相邻子问题之间的关系(递推式)

  重叠子问题,对每个子问题只计算一次,然后将其计算的结果保存到一个表格中,每一次需要上一个子问题解时,进行调用,只要o(1)时间复杂度,准确的说,动态规划是利用空间去换取时间的算法.

  判断是否可以利用动态规划求解,第一个是判断是否存在重叠子问题

2、模板

分析模板

  1. 状态表示:dp[i]表示一类情况的集合
    • 集合,根据不同种类动态规划问题分析划分不同的集合
    • 属性有:Max/Min, Bool, Count
  2. 状态计算:集合划分
    • 划分原则:不遗漏,不重复(最值可重复)
    • 划分依据:寻找最后一个不同点

     3、初始化

3、实例

10. 正则表达式匹配

难度:中等

题目描述

给你一个字符串 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

Solution

  1. 动态规划

  • 状态表示:dp[m + 1][n + 1]
    • dp[i][j]表示s的前i个字母和p的前j个字母是否匹配
    • 属性:bool;
  • 状态计算:有一个类似完全背包的简化推导
    • p[j] == '*':dp[i][j] = dp[i][j - 2] || (dp[i - 1][j] && s[i] == p[j - 1])  ;  * 可匹配0, 1, 2 ...个字母; dp[i][j] = dp[i][j - 2]表示匹配0次,(dp[i - 1][j] && s[i] == p[j - 1]) 表示s[i]至少匹配一次
    • p[j] == '.': dp[i][j] = dp[i - 1][j - 1];'.'表示可以匹配任意字符
    • p[j] is letter: dp[i][j] = dp[i - 1][j - 1] && s[i] == p[j]
  • 初始化:
    • dp[0][0] = true;
    • p[j - 1)]== '*' : dp[0][j] = dp[0][j - 2]
class Solution:
    def isMatch(self, s: str, p: str) -> bool:
        m, n = len(s), len(p)

        def matches(i: int, j: int) -> bool:
            if i == 0:
                return False
            if p[j - 1] == '.':
                return True
            return s[i - 1] == p[j - 1]

        f = [[False] * (n + 1) for _ in range(m + 1)]
        f[0][0] = True
        for i in range(m + 1):
            for j in range(1, n + 1):
                if p[j - 1] == '*':
                    f[i][j] |= f[i][j - 2]
                    if matches(i, j - 1):
                        f[i][j] |= f[i - 1][j]
                else:
                    if matches(i, j):
                        f[i][j] |= f[i - 1][j - 1]
        return f[m][n]

时间复杂度:O(mn),其中 m 和 n分别是字符串 s 和 p 的长度。我们需要计算出所有的状态,并且每个状态在进行转移时的时间复杂度为 O(1)。

空间复杂度:O(mn),即为存储所有状态使用的空间。

https://leetcode-cn.com/problems/regular-expression-matching/solution/zheng-ze-biao-da-shi-pi-pei-by-leetcode-solution/

1143. 最长公共子序列

难度:中等

题目描述

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。

若这两个字符串没有公共子序列,则返回 0。

示例 1:

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace",它的长度为 3。
示例 2:

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3。
示例 3:

输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0。

提示:

  • 1 <= text1.length <= 1000
  • 1 <= text2.length <= 1000
  • 输入的字符串只含有小写英文字符。

动态规划

  • 状态表示:C[i][j] text1[0~i] 与 text2[0~j]的最长公共子序列的长度; 
  • 状态计算:找最后一个不同点。text1的第i位字符和text2的第j位字符是否在这个最长公共子序列中

       存储自底向上求解子问题的结果(分别长度为i和j时的最长公共子序列长度)。递归方程为:
      (1)if text1[i] == text2[j]: C[i][j] = C[i-1][j-1] + 1 .
      (2)elif C[i-1][j] > C[i][j-1] : C[i][j] = C[i-1][j]
      (3)else: C[i][j] = C[i][j-1]

  • 初始化:求的是最长的公众子序列,就初始化为0吧
class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        m = len(text1)
        n = len(text2)
        C = [[0 for i in range(n+1)] for j in range(m+1)]
        for i in range(1, m+1):
            for j in range(1, n+1):
                if text1[i-1] == text2[j-1]:
                    C[i][j] = C[i-1][j-1] + 1
                else:
                    if C[i-1][j] > C[i][j-1]:
                        C[i][j] = C[i-1][j]
                    else:
                        C[i][j] = C[i][j-1]
        return C[-1][-1]

时间复杂度:O(mn),m、n分别为两个字符串的长度。
空间复杂度:O(mn),m、n分别为两个字符串的长度。
https://leetcode-cn.com/problems/longest-common-subsequence/solution/1143-zui-chang-gong-gong-zi-xu-lie-by-jue-qiang-zh/ 

 

120. 三角形最小路径和

难度:中等

题目描述

给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。

相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。

例如,给定三角形:

[
     [2],
    [3,4],
   [6,5,7],
  [4,1,8,3]
]
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。

思路:

方案1:要求空间复杂度尽可能小,由底向上,在原数组上改动

方案2:从底向上,只使用一个一维数组记录方案2、

 

方案3:

  • 状态表示:dp[i][j] 表示到达i行j列的最小路径和; 属性:Min;
  • 状态计算:边界点只有一条路径,中间节点有两条路径
    • 第一列: dp[i][0] = dp[i - 1][0] + triangle[i][0]
    • 其他列:dp[i][j] = min(dp[i - 1][j], dp[i - 1][j - 1]) + triangle[i][j]
  • 初始化:dp直接为三角形的拷贝
class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        rows = len(triangle)
        for i in range(rows - 2, -1, -1):
            cols = len(triangle[i])
            for j in range(cols):
                triangle[i][j] += min(triangle[i+1][j], triangle[i+1][j+1])
        return triangle[0][0]
class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        dp = triangle[-1]
        rows = len(triangle)
        for i in range(rows-2, -1, -1):
            cols = len(triangle[i])
            for j in range(cols):
                dp[j] = min(dp[j], dp[j+1]) + triangle[i][j]
        return dp[0]
class Solution(object):
    def minimumTotal(self, triangle):
        dp = triangle                   #这边的初始化真的是牛逼,不仅不需要把把上面两行特殊的情况考虑进去,而且为下面也做了铺垫。       
        m = len(dp)

        for i in range(1, m):
            for j in range(i+1):
                if j == 0:
                    dp[i][j] += dp[i-1][j]         #第一列  
                if j> 0 and j == i:
                    dp[i][j] += dp[i-1][j-1]       #i=j列 
                    
                elif (j > 0 and j < i):            #j>0 and j<i列 
                    dp[i][j] += min(dp[i-1][j-1],dp[i-1][j])
        return (min(dp[-1]))

https://leetcode-cn.com/problems/triangle/solution/you-shang-zhi-xia-you-xia-zhi-shang-by-tian-di-jin/

https://leetcode-cn.com/problems/triangle/solution/trianglede-zuo-ti-guo-cheng-he-si-lu-by-nonentity-/

53. 最大连续子序和

难度:简单

题目描述

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

示例:

输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。

1. 暴力求解
基本思路就是遍历一遍,用两个变量,一个记录最大的和,一个记录当前的和。时空复杂度貌似还不错(时间复杂度 O(n),空间复杂度 O(l)

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        local_max = nums[0]
        global_max = local_max
        for i in range(1,len(nums)):
            # 当当前序列加上此时的元素的值大于local_max的值,说明最大序列和可能出现在后续序列中,记录此时的最大值
            if local_max + nums[i]>nums[i]:
                local_max=local_max+nums[i]
                global_max=max(global_max,local_max)
            else:
            #当local_max(当前和)小于下一个元素时,当前最长序列到此为止。以该元素为起点继续找最大子序列,
            # 并记录此时的最大值
                local_max=nums[i]
                global_max= max(global_max,local_max)
        return global_max

2、动态规划

  • 状态表示:dp[i]表示以nums[i]结尾的最大子数组和;
  • 状态计算:dp[i] = Math.max(0, dp[i - 1]) + nums[i]
    • 如果dp[i - 1] 为负数的话,那就从nums[i]重新开始计算数组;;
    • 如果dp[i - 1] 为正数的话,加上nums[i]就行;
  • 初始化:全部赋值为0, dp[0] = nums[0]--因为子数组最少包含一个元素。
class Solution:
    def maxSubArray(self, nums) -> int:
        dp = [0 for _ in range(len(nums))]
        dp[0] = nums[0]
        for i in range(1, len(nums)):
            if nums[i] > nums[i] + dp[i-1]:
                dp[i] = nums[i]
            else:
                dp[i] = nums[i] + dp[i-1]
                
        return max(dp)

链接:https://leetcode-cn.com/problems/maximum-subarray/solution/zui-da-zi-xu-he-python3dong-tai-gui-hua-ti-jie-by-/

63. 不同路径 II

难度:中等

题目描述

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 1 和 0 来表示。

说明:m 和 n 的值均不超过 100。

示例 1:

输入:
[
  [0,0,0],
  [0,1,0],
  [0,0,0]
]
输出: 2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

Solution

  1. 动态规划

  • 状态表示:dp[i][j] 表示 走到i行j列的路径的数量; 属性:count;
  • 状态计算:
    • 有障碍:dp[i][j] = 0
    • 无障碍:dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
  • 初始化: dp[0][j]和dp[i][0]无障碍顺延dp[0][j]=dp[j-1];dp[i][0]=dp[i-1][0],dp[0][0]=0;遇到障碍之后都为0,因为只有一条路。
class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        m, n = len(obstacleGrid), len(obstacleGrid[0])
        if obstacleGrid[0][0] == 1:
            return 0
        dp = [[0 for _ in range(n)] for _ in range(m)]
        dp[0][0] = 1
        for i in range(1, m):
            if obstacleGrid[i][0] == 0:
                dp[i][0] = dp[i-1][0]
        for j in range(1, n):
            if obstacleGrid[0][j] == 0:
                dp[0][j] = dp[0][j-1]
        for i in range(1, m):
            for j in range(1, n):
                if obstacleGrid[i][j] == 1:
                    dp[i][j] = 0
                else:
                    dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
        return dp[m - 1][n - 1]

https://leetcode-cn.com/problems/unique-paths-ii/solution/dong-tai-gui-hua-by-ly_xian-2/

 

72. 编辑距离

难度:困难

题目描述

给你两个单词 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')

Solution

  1. 动态规划

这三个操作对一个字符串操作就行了。

  • 状态表示:dp[i][j] 表示 word1 的前 i 个字母转化为 word2 的前 j 字母的操作数; 属性:Min;
  • 状态计算:找最后一个不同点。
    • word1 删除一个字符 dp[i - 1][j] + 1
    • word1 增加一个字符 dp[i][j - 1] + 1
    • word1 不用变 dp[i - 1][j - 1]
    • word1 替换一个字符 dp[i - 1][j - 1] + 1

 总结:

         word1[i]==word2[j]时:dp[i][j] = dp[i-1][j-1]

          word1[i]!=word2[j]时:min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1] ) + 1

  • 初始化:属性为Min,dp[m + 1][n + 1]先初始化为MAX_VALUE`
    • dp[i][0] = i;
    • dp[0][j] = j;
class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        n1 = len(word1)
        n2 = len(word2)
        dp = [[0] * (n2 + 1) for _ in range(n1 + 1)]
        # 第一行
        for j in range(1, n2 + 1):
            dp[0][j] = dp[0][j-1] + 1
        # 第一列
        for i in range(1, n1 + 1):
            dp[i][0] = dp[i-1][0] + 1
        for i in range(1, n1 + 1):
            for j in range(1, n2 + 1):
                if word1[i-1] == word2[j-1]:
                    dp[i][j] = dp[i-1][j-1]
                else:
                    dp[i][j] = min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1] ) + 1
        #print(dp)      
        return dp[-1][-1]

91. 解码方法

难度:中等

题目描述

一条包含字母 A-Z 的消息通过以下方式进行了编码:

'A' -> 1
'B' -> 2
...
'Z' -> 26

给定一个只包含数字的非空字符串,请计算解码方法的总数。

示例 1:

输入: "12"
输出: 2
解释: 它可以解码为 "AB"(1 2)或者 "L"(12)。
示例 2:

输入: "226"
输出: 3
解释: 它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。

解题思路:

  • 状态表示:dp[i] 表示以i结尾的解码方法; 属性:count;
  • 状态计算:
    • s[i] 和 s[i - 1] 能否两位一起解码以及能够分别单个解码;
    • s[i]存在字典中,即s[i] != '0'(因为测试都是数码串,因此可以这么写),此时解码s[:i+1]可以通过解码字符子串s[:i]和最后一个字符s[i]得到
    • s[i-1:i+1] (即s[i-1]和s[i]构成的字符串)在字典中,在python中即 s[i-1:i+1]>='10 and s[i-1:i+1]<= '26',此时解码s[:i+1]也可以通过解码字符子串s[:i-1]和最后两个个字符s[i-1:i+1]得到
    • 归纳如下:
      • dp[i+1] = 0. if s[i] == '0' and (s[i-1:i+1] < '10' or s[i-1:i+1]>'26')
        dp[i+1] = dp[i]. if s[i]!= '0' and (s[i-1:i+1] < '10' or s[i-1:i+1]>'26')
        dp[i+1] = dp[i-1]. if s[i] == '0' and (s[i-1:i+1] >= '10' and s[i-1:i+1]<='26')
        dp[i+1] = dp[i]+dp[i-1]. if s[i] != '0' and (s[i-1:i+1] >= '10' and s[i-1:i+1]<='26')

      初始化:

       动态规划中常用的padding,目的是使得边界的转移方程和非边界的一样。如果不加padding,边界的值要单独计算。

      一个边界的设定,可以理解为s[:0]即空字符串“”的解码方法种类——方法只有一个就是不用作任何解码处理。你可以从dp[2]的计算简单验证这一个设定是正确的,如果前两位字符在字典中,那么他们通过把前两位字符一同解码而不是分开解码,可以得到dp[0]也就是1个解码方案。

https://leetcode-cn.com/problems/decode-ways/solution/dong-tai-gui-hua-cong-jian-dan-de-pa-lou-ti-wen-ti/

class Solution:
    def numDecodings(self, s: str) -> int:
        n = len(s)
        if n==0: return 0
        dp = [1,0]
        dp[1] = 1 if s[0]!='0' else 0 
        for i in range(1,n):
            dp.append(0)
            if s[i]!='0':
                dp[i+1] += dp[i]
            if s[i-1:i+1]>='10' and s[i-1:i+1]<='26':
                dp[i+1] += dp[i-1]
        
        return dp[-1]

198. 打家劫舍

难度:简单

题目描述

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示

  • 0 <= nums.length <= 100
  • 0 <= nums[i] <= 400
  • 思路
  • 状态表示:dp[i] 表示决定完第i家的所能取到的最大值; 属性:Max;
  • 状态计算:
    • 偷不偷第i家,选个最大值:dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
  • 初始化:
    • 初始化为0。
class Solution(object):
    def rob(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        # 每一个cur从开始到偷窃到i房屋的金额最优解,不能简单理解为奇偶问题!!!

        # 优化空间复杂度为O(1),时间复杂度O(n)
        '''
        cur,pre = 0,0
        for i in nums:
            cur,pre = max(pre+i,cur),cur
        return cur
        '''

        # 非优化空间复杂度O(n),时间复杂度O(n)

        n = len(nums)
        dp = [0]*(n+2)
        for i in range(2,n+2):
            dp[i] = max(dp[i-1],dp[i-2]+nums[i-2])
        return dp[-1]

https://leetcode-cn.com/problems/house-robber/solution/python-dong-tai-gui-hua-by-yu-fa-tang-you-dian-tia/

337. 打家劫舍 III

难度:中等

题目描述

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

示例 1:

输入: [3,2,3,null,3,null,1]

     3
    / \
   2   3
    \   \
     3   1

输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
示例 2:

输入: [3,4,5,1,3,null,1]

     3
    / \
   4   5
  / \   \
 1   3   1

输出: 9
解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.

Solution

动态规划,二叉树后序遍历。

  • 状态表示:
    • dp[i][0] 表示抢劫第i家的金额;
    • dp[i][1] 表示不抢劫第i家的金额;
    • 属性:Max;
    • TreeNode的问题;
  • 状态计算: (树中直接相连被偷会报警)
    • 偷第i家:dp[i][0] = i.val + dp[i.left][1] + dp[i.right][1];
    • 不偷第i家:dp[i][1] = max(dp[i.left][1], dp[i.left][0]) + max(dp[i.right][1], dp[i.right][0]);
  • 初始化:w[i]为i的收益
    • dp[i][1] = 0;
    • dp[i][0] = w[i];
class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

class Solution:
    def rob(self, root: TreeNode) -> int:
        a = self.helper(root)   # a 是一个二维数组, 为root的[偷值, 不偷值]
        return max(a[0], a[1])  # 返回两个值的最大值, 此值为小偷最终获得的总值
    
    # 参数为root节点, helper方法输出一个二维数组:root节点的[偷值, 不偷值]
    def helper(self, root):     # 递归结束条件:root为空, 输出 [0, 0]
        if not root:
            return [0, 0]
        left = self.helper(root.left)   # left是一个二维数组, 为 root 左侧子节点的[偷值, 不偷值]
        right = self.helper(root.right) # right也是一个二维数组, 为root右侧子节点的[偷值, 不偷值]
        robValue = left[1] + right[1] + root.val    # root 的偷值
        skipValue = max(left[0], left[1]) + max(right[0], right[1]) # root 的不偷值
        return [robValue, skipValue]    # 输出小偷可获得的最大金额

https://leetcode-cn.com/problems/house-robber-iii/solution/dfszui-zhi-guan-de-fang-fa-xiang-xi-zhu-shi-by-men/

300. 最长上升子序列

难度:中等

题目描述

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

说明:

可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。 你算法的时间复杂度应该为 O(n2) 。

Solution

  1. 动态规划

  • 状态表示:dp[i] 表示以nums[i] 为结尾的最长上升子序列的长度; 属性:Max;
  • 状态计算:遍历j [0~i-1], 比较 nums[j] 与 nums[i],找到以i结尾的最长子序列长度;
    • if(nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
  • 初始化:全部初始化为1,因为单个字母也是一个子序列

https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-by-leetcode-soluti/

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        if not nums:
            return 0
        dp = []
        for i in range(len(nums)):
            dp.append(1)
            for j in range(i):
                if nums[i] > nums[j]:
                    dp[i] = max(dp[i], dp[j] + 1)
        return max(dp)

时间复杂度:O(n^2),其中 n为数组nums 的长度。动态规划的状态数为 n,计算状态 dp[i]时,需要 O(n)的时间遍历 dp[0…i−1] 的所有状态,所以总时间复杂度为 O(n^2)

空间复杂度:O(n),需要额外使用长度为 n的 dp 数组。

https://leetcode-cn.com/problems/longest-increasing-subsequence/solution/zui-chang-shang-sheng-zi-xu-lie-by-leetcode-soluti/
 

312. 戳气球

难度:困难

题目描述

有 n 个气球,编号为0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中。

现在要求你戳破所有的气球。如果你戳破气球 i ,就可以获得 nums[left] * nums[i] * nums[right] 个硬币。 这里的 left 和 right 代表和 i 相邻的两个气球的序号。注意当你戳破了气球 i 后,气球 left 和气球 right 就变成了相邻的气球。

求所能获得硬币的最大数量。

说明:

  • 你可以假设 nums[-1] = nums[n] = 1,但注意它们不是真实存在的所以并不能被戳破。
  • 0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100


输入: [3,1,5,8]
输出: 167
解释: nums = [3,1,5,8] --> [3,5,8] -->   [3,8]   -->  [8]  --> []
     coins =  3*1*5      +  3*5*8    +  1*3*8      + 1*8*1   = 167

思路:

状态定义:

dp[i][j] = x 表示戳破气球 i 和气球 j 之间(开区间,不包括 i 和 j)的所有气球,可以获得的最大***数为 x。
状态计算:
而对于 dp[i][j],i 和 j 之间会有很多气球,到底该戳哪个先呢?我们直接设为 k,枚举选择最优的 k 就可以了。
所以,最终的状态转移方程为:dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] + nums[k] * nums[i] * nums[j])。由于是开区间,因此 k 为 i + 1, i + 2... j - 1。

注意 i 不一定是 k - 1,同理 j 也不一定是 k + 1,因此可能 i - 1 和 i + 1 已经被戳破了。

初始值和边界:

  • 由于我们利用了两个虚拟气球,边界就是气球数 n + 2
  • 初始值,当 i == j 时,很明显两个之间没有气球,所有为 0;

https://leetcode-cn.com/problems/burst-balloons/solution/dong-tai-gui-hua-js312-chuo-qi-qiu-by-fe-lucifer/
 

class Solution:
    def maxCoins(self, nums: List[int]) -> int:
        n = len(nums)
        points = [1] + nums + [1]
        dp = [[0] * (n + 2) for _ in range(n + 2)]

        for i in range(n, -1, -1):
            for j in range(i + 1, n + 2):
                for k in range(i + 1, j):
                    dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] + points[i] * points[k] * points[j])
        return dp[0][-1]

518. 零钱兑换 II

难度:中等

题目描述

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

示例 1:

输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

示例 2:

输入: amount = 3, coins = [2]
输出: 0
解释: 只用面额2的硬币不能凑成总金额3。

示例 3:

输入: amount = 10, coins = [10]
输出: 1

注意:

你可以假设:

  • 0 <= amount (总金额) <= 5000
  • 1 <= coin (硬币面额) <= 5000
  • 硬币种类不超过 500 种
  • 结果符合 32 位符号整数

方案1:

  • 状态定义:
  • 用 dp[i] 来表示组成 i 块钱,需要最少的硬币数,那么
  • 状态转移:
  • 第 j 个硬币我可以选择不拿 这个时候, 组成数 = dp[i]
  • 第 j 个硬币我可以选择拿 这个时候, 组成数 = dp[i - coins[j]] + dp[i]
  • 初始化:
  • dp = [0] * (amount + 1)
  • dp[0] = 1

方案2:

  • 状态表示      
  • 动态规划:题目求组合数,设dp[i][j]表示使用前i种***组成j金额的组合数。
  • 状态转移 :
  • dp[i][j] = dp[i-1][j] + dp[i][j-coins[i]]
  • 初始化:
  • dp = [[0 for _ in range(len(coins) + 1)] for _ in range(amount + 1)]
  • for j in range(len(coins) + 1):
  •        dp[0][j] = 1
class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        dp = [0] * (amount + 1)
        dp[0] = 1

        for j in range(len(coins)):
            for i in range(1, amount + 1):
                if i >= coins[j]:
                    dp[i] += dp[i - coins[j]]

        return dp[-1]
class Solution:
    def change(self, amount: int, coins: List[int]) -> int:
        dp = [[0 for _ in range(len(coins) + 1)] for _ in range(amount + 1)]
        for j in range(len(coins) + 1):
            dp[0][j] = 1

        for i in range(amount + 1):
            for j in range(1, len(coins) + 1):
                if i >= coins[j - 1]:
                    dp[i][j] = dp[i - coins[j - 1]][j] + dp[i][j - 1]
                else:
                    dp[i][j] = dp[i][j - 1]
        return dp[-1][-1]

 

https://leetcode-cn.com/problems/coin-change-2/solution/518-ling-qian-dui-huan-ii-you-hua-dong-tai-gui-hua/

664. 奇怪的打印机

难度:困难

题目描述

有台奇怪的打印机有以下两个特殊要求:

  • 打印机每次只能打印同一个字符序列。
  • 每次可以在任意起始和结束位置打印新字符,并且会覆盖掉原来已有的字符。

给定一个只包含小写英文字母的字符串,你的任务是计算这个打印机打印它需要的最少次数。

示例 1:

输入: "aaabbb"
输出: 2
解释: 首先打印 "aaa" 然后打印 "bbb"。

示例 2:

输入: "aba"
输出: 2
解释: 首先打印 "aaa" 然后在第二个位置打印 "b" 覆盖掉原来的字符 'a'。
提示: 输入字符串的长度不会超过 100。

思路:

区间DP模板是三重循环

  • 一重循环:区间长度
  • 二重循环:区间起点
  • 三重循环:区间分割点

  • 状态表示:dp[n + 1][n + 1]

    • dp[i][j] 表示打印区间[i,j)位置字符串的次数;
    • 属性:Min
  • 状态计算:k = i ~ j

    • 假设第i个字母后这个区间的每一个字母都不相同,dp[i][j] = 1 + dp[i + 1][j]
    • 假设区间里有字母和区间首部元素相同,那么最小值一定出现在这样的打印方案中。if(s[k] == s[i]) dp[i][j] = Math.min(dp[i][j], dp[i][k - 1] + dp[k + 1][j])
  • 初始化:

    • 因为区间是从小到到大来动态规划,所以只需要初始化区间长度为1的就行,一个字母最少也要打印一次,dp[i][i] = 1;
class Solution:
    def strangePrinter(self, s: str) -> int:
        n = len(s)
        dp = [[0]*(n+1) for _ in range(n+1)]
        
        #自己到自己最小次数为1
        for i in range(n):
            dp[i][i+1]=1
            
        #确定区间长度
        for l in range(1, n+1):
            #确定起始位置
            for i in range(n+1-l):
                j=i+l
                #后面所有字符都不同,仅仅打印元素[i,i+1]元素;dp[i][j] = dp[i+1][j]+1
                dp[i][j] = dp[i+1][j]+1
                
                #存在和首字母相同的元素;寻找最优区间分割点k
                for k in range(i+1, j):
                    if s[k]==s[i]:
                        dp[i][j]=min(dp[i][j], dp[i][k]+dp[k+1][j])
        return dp[0][n]  

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值