【动态规划】其余练习 (一)

目录

一、LC 42. 接雨水 ☆

1.1 题求

1.2 求解

二、LC.62 不同路径

2.1 题求

2.2 求解

三、LC.63 不同路径 II

3.1 题求

3.2 求解

四、LC.91 解码方法

4.1 题求

4.2 求解

五、LC.120 三角形最小路径和

5.1 题求

5.2 求解

六、LC.131 分割回文串

6.1 题求

6.2 求解

七、LC.132 分割回文串 II

7.1 题求

7.2 求解

八、LC.139 单词拆分

8.1 题求

8.2 求解

九、LC.140 单词拆分 II

9.1 题求

9.2 求解

十、LC.152 乘积最大子数组

10.1 题求

10.2 求解


一、LC 42. 接雨水 ☆

1.1 题求

1.2 求解

# 36ms - 88.19%
class Solution:
    def trap(self, height: List[int]) -> int:
        if not height:
            return 0
        
        n = len(height)
        
        # 从左往右
        leftMax = [0] * n
        leftMax[0] = height[0]
        for i in range(1, n):
            # leftMax[i] 表示下标 i 及其左侧 height 的最大高度
            leftMax[i] = max(leftMax[i-1], height[i])
        
        # 从右往左
        rightMax = [0] * n
        rightMax[-1] = height[-1]
        for i in range(n-2, -1, -1):
            # rightMax[i] 表示下标 i 及其右侧 height 的最大高度
            rightMax[i] = max(rightMax[i+1], height[i])

        # 结果
        # 下标 i 处的水能到达的最大高度取决于左右侧最大高度中的较小者
        # 下标 i 处能接的雨水量等于下标 i 处的水能到达的最大高度减 height[i]
        res = sum(min(leftMax[i], rightMax[i]) - height[i] for i in range(n))
        return res

# 40ms - 74.99%
class Solution:
    def trap(self, height: List[int]) -> int:
        res = 0
        stack = []  # 存储柱子高度索引 i
        n = len(height)
        
        # 从左往右遍历
        for i, h in enumerate(height):
            # 若栈非空且当前高度 h 大于栈顶高度 height[stack[-1]]
            # 则 h 构成当前右边界高度, 索引为 i
            while stack and h > height[stack[-1]]:
                # 栈顶高度索引 top 出栈 - height[top] 是低于 h 的凹槽位
                top = stack.pop()  
                # 若栈空直接结束 - 因为没有左边界 left 了
                if not stack:
                    break
                # 当前栈顶高度索引 left (必满足 height[left] > height[top])
                left = stack[-1]
                # 可接雨水区域的 宽度
                currWidth = i - left - 1
                # 可接雨水区域的 高度
                currHeight = min(height[left], height[i]) - height[top]
                # 可接雨水区域的 面积 - (总体上看是分层计算累加的)
                res += currWidth * currHeight
            # 当前索引入栈 - 作为后续潜在的左边界
            stack.append(i)
        
        return res

# 40ms - 74.99%
class Solution:
    def trap(self, height: List[int]) -> int:
        res = 0
        left, right = 0, len(height) - 1
        leftMax = rightMax = 0
        
        # 双指针中央靠拢直至相遇
        while left < right:
            # 左、右最大值
            leftMax = max(leftMax, height[left])
            rightMax = max(rightMax, height[right])
            # left 较小, 能接的雨水取决于与 leftMax 之差
            if height[left] < height[right]:
                res += leftMax - height[left]
                left += 1
            # right 较小, 能接的雨水取决于与 rightMax 之差    
            else:
                res += rightMax - height[right]
                right -= 1
        
        return res

参考资料:

力扣

力扣


二、LC.62 不同路径

2.1 题求

2.2 求解

法一:动态规划 - 基本版

# 28ms - 90.46%
class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        
        # dp 数组及其初始化 - dp[i][j] 表示到达 (i, j) 的不同路径数
        dp = [[0 for _ in range(n)] for _ in range(m)]
        # 第 0 行和第 0 列均只有 1 种路径
        for i in range(m):
            dp[i][0] = 1
        for j in range(n):
            dp[0][j] = 1
        
        # 状态转移
        for i in range(1, m):
            for j in range(1, n):
                # 当前位置路径数等于相邻的上格路径数 + 左格路径数
                dp[i][j] = dp[i-1][j] + dp[i][j-1]
        
        return dp[m-1][n-1]

法二:动态规划 - 降维版 / 滚动数组

# 24ms - 97.60%
class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        # dp 数组及其初始化 - dp[j] 表示到达第 j 列的不同路径数
        dp = [1 for _ in range(n)]  # 第 0 列均只有 1 条路径
        # 状态转移
        for _ in range(1, m):
            for k in range(1, n):
                dp[k] += dp[k-1] 
        # 最后一行、列
        return dp[n-1]

官方说明

# 20ms - 99.67%
class Solution:
    def uniquePaths(self, m: int, n: int) -> int:
        # 理解:已知从 (0, 0) 到 (m-1, n-1) 共需走 m + n - 2 步
        # 若将向右走视为无差别, 则区别全依赖于何时往下走, 故共有 C_{m+n-2}^{n-1} 种
        # 同理, 也可理解为有 C_{m+n-2}^{m-1} 种

        from math import comb
        return comb(m + n - 2, n - 1)
        # return comb(m + n - 2, m - 1)  # 同理

参考资料:

力扣


三、LC.63 不同路径 II

3.1 题求

3.2 求解

法一:动态规划

# 24ms - 98.34%
class Solution:
    def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
        if obstacleGrid[0][0] == 1 or obstacleGrid[-1][-1] == 1:
            return 0
        
        # 行、列数
        m, n = len(obstacleGrid), len(obstacleGrid[0])
        # dp 数组
        dp = [[0 for _ in range(n)] for _ in range(m)]
        # dp 数组初始化
        # 原点 / 起点
        dp[0][0] = 1
        # 第 0 列
        for i in range(1, m):
            dp[i][0] = 0 if obstacleGrid[i][0] == 1 else dp[i-1][0] 
        # 第 0 行
        for j in range(1, n):
            dp[0][j] = 0 if obstacleGrid[0][j] == 1 else dp[0][j-1] 
        
        # 状态转移
        for i in range(1, m):
            for j in range(1, n):
                dp[i][j] = 0 if obstacleGrid[i][j] == 1 else dp[i-1][j] + dp[i][j-1]
    
        return dp[m-1][n-1]

参考资料:

力扣


四、LC.91 解码方法

4.1 题求

4.2 求解

法一:动态规划 - 基本版 (1维 DP 数组)

# 24ms - 97.79%
class Solution:
    def numDecodings(self, s: str) -> int:
        if s[0] == '0':
            return 0

        # dp[i] 表示使用前 i 个数字可构成的解码总数
        n = len(s)
        dp = [0 for _ in range(n+1)]
        # 无数和单数都算一种
        dp[0] = dp[1] = 1 
        
        for i in range(2, n+1):
            # 1. 当前位 s[i-1] 为 0
            if s[i-1] == '0':
                # 若前一位 s[i-2] 不为 1 或 2, 则不符
                if s[i-2] != '1' and s[i-2] != '2':
                    return 0
                
                # 若前一位 s[i-2] 为 1 或 2,
                # 则构成 '10' 或 '20', 当且仅当 dp[i-2] + s[i-2:i] 算一种
                else:
                    dp[i] = dp[i-2] 

            # 2. 当前位 s[i-1] 不为 0
            else:
                # 若前一位 s[i-2] 为 0, 则只能自成一种; 
                # 若前一位 s[i-2] 不为 0, 但不符合数值范围, 也只能自成一种
                if s[i-2] == '0' or int(s[i-2:i]) > 26:
                    dp[i] = dp[i-1]
                
                # 若前一位 s[i-2] 不为 0, 且符合数值范围
                # 则 dp[i-2] + s[i-2:i] 算一种, dp[i-1] + s[i-1:i] 算一种 
                else:
                    dp[i] = dp[i-1] + dp[i-2] 

        return dp[n]

法二 - 动态规划 - 降维版 (滚动数组)

class Solution:
    def numDecodings(self, s: str) -> int:
        if s[0] == '0':
            return 0

        # dp[i] 表示使用前 i 个数字可构成的解码总数
        # 使用 3 个变量代表相邻的 3 个 dp[i-2], dp[i-1], dp[i]
        left, mid, right = 1, 1, 1
        
        for i in range(2, len(s)+1):
            # 1. 当前位 s[i-1] 为 0
            if s[i-1] == '0':
                # 若前一位 s[i-2] 不为 1 或 2, 则不符
                if s[i-2] != '1' and s[i-2] != '2':
                    return 0
                
                # 若前一位 s[i-2] 为 1 或 2,
                # 则构成 '10' 或 '20', 当且仅当 dp[i-2] + s[i-2:i] 算一种
                else:
                    right = left  # dp[i] = dp[i-2] 

            # 2. 当前位 s[i-1] 不为 0
            else:
                # 若前一位 s[i-2] 为 0, 则只能自成一种; 
                # 若前一位 s[i-2] 不为 0, 但不符合数值范围, 也只能自成一种
                if s[i-2] == '0' or int(s[i-2:i]) > 26:
                    right = mid  # dp[i] = dp[i-1]
                
                # 若前一位 s[i-2] 不为 0, 且符合数值范围
                # 则 dp[i-2] + s[i-2:i] 算一种, dp[i-1] + s[i-1:i] 算一种 
                else:
                    right = left + mid  # dp[i] = dp[i-1] + dp[i-2]

            left, mid = mid, right  # 更新

        return right  # dp[n]

参考资料:

力扣


五、LC.120 三角形最小路径和

5.1 题求

5.2 求解

法一:动态规划 - 基本版 (2 维 DP 数组)

# 32ms - 91.35%
class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        '''
        2
        3 4
        6 5 7
        4 1 8 3
        '''
        # dp[i][j] 代表达到第 i 行 j 列的最小路径和
        m = len(triangle)
        dp = [[0 for _ in range(m)] for _ in range(m)]
        dp[0][0] = triangle[0][0]
        
        for i in range(1, m):
            dp[i][0] = dp[i-1][0] + triangle[i][0]    # 最左路径
            dp[i][i] = dp[i-1][i-1] + triangle[i][i]  # 最右路径
            # 中间路径
            for j in range(1, i):
                dp[i][j] = min(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j]
        
        return min(dp[-1])

法二:动态规划 - 降维版 (1 维 DP 数组)

class Solution:
    def minimumTotal(self, triangle: List[List[int]]) -> int:
        # dp[k] 当前行代表达到第 k 列的最小路径和
        m = len(triangle)
        dp = [0 for _ in range(m)]
        dp[0] = triangle[0][0]
        
        # 每行从右往左遍历
        for i in range(1, m):
            # 最右路径
            dp[i] = dp[i-1] + triangle[i][i]  
            # 中间路径
            for j in range(i-1, 0, -1):
                dp[j] = min(dp[j], dp[j-1]) + triangle[i][j]
            # 最左路径
            dp[0] += triangle[i][0]    
        
        return min(dp)

参考资料:

力扣


六、LC.131 分割回文串

6.1 题求

6.2 求解

# 100ms - 88.67%
class Solution:
    def partition(self, s: str) -> List[List[str]]:
        ## 1. DP 预处理回文串
        n = len(s)
        # f[i][j] 表示 s[i:j+1] 是否为回文串
        f = [[True for _ in range(n)] for _ in range(n)]  
        # 从右往左
        for i in range(n-1, -1, -1):
            # 从左往右
            for j in range(i+1, n):
                # 状态转移 ☆
                f[i][j] = (s[i] == s[j]) and f[i+1][j-1] 

        ans = []  # 单个结果
        ret = []  # 所有结果

        ## 2. 回溯 + DFS 搜索答案
        def dfs(i: int):
            # 遍历完所有字符串, 记录结果
            if i == n:
                ret.append(ans[:])
                return
            
            # 遍历 j∈[i, n) 找到所有以 i 为起点索引的回文串
            for j in range(i, n):
                if f[i][j]:
                    ans.append(s[i:j+1])  # s[s:j+1] 构成回文串
                    dfs(j+1)  # 下一个回文串以 j+1 为起点索引
                    ans.pop()  # 弹出 s[s:j+1] 试探下一个回文串
        dfs(0)
        return ret

# 96ms - 92.88%
class Solution:
    def partition(self, s: str) -> List[List[str]]:
        # 记忆化搜索确定回文串
        from functools import lru_cache
        @lru_cache  # 1 - True, others - False
        def isPalindrome(i: int, j: int) -> int:
            if i >= j:
                return 1
            return isPalindrome(i+1, j-1) if s[i] == s[j] else -1

        ## 2. 回溯 + DFS 搜索答案
        def dfs(i: int):
            # 遍历完所有字符串, 记录结果
            if i == n:
                ret.append(ans[:])
                return
            
            # 遍历 j∈[i, n) 找到所有以 i 为起点索引的回文串
            for j in range(i, n):
                if isPalindrome(i, j) == 1:
                    ans.append(s[i:j+1])  # s[s:j+1] 构成回文串
                    dfs(j+1)  # 下一个回文串以 j+1 为起点索引
                    ans.pop()  # 弹出 s[s:j+1] 试探下一个回文串
                       
        n = len(s)
        ans = []  # 单个结果
        ret = []  # 所有结果
        
        dfs(0)
        return ret

参考资料:

力扣

力扣


七、LC.132 分割回文串 II

7.1 题求

7.2 求解

法一:动态规划

# 700ms - 46.89% - 使用 lru_cache 方法则会超时(毕竟是递归)
class Solution:
    def minCut(self, s: str) -> int:

        min_times = n = len(s)
        
        # dp1[i][j] 表示从 i 到 j 是否为回文串
        dp1 = [[True for _ in range(n)] for _ in range(n)]
        for i in range(n-1, -1, -1):
            for j in range(i+1, n):
                dp1[i][j] = (s[i] == s[j]) and dp1[i+1][j-1]
        
        # dp2[k] 表示从 0 到 k 符合要求的最小分割次数
        dp2 = [n for _ in range(n)]

        # 遍历右索引 j∈[0: n]
        for j in range(n):
            # 若 s[0: j+1] 为回文串, 则无需分割次数
            if dp1[0][j]:
                dp2[j] = 0
            # 否则, 必须多分割一次
            else:
                # 遍历左索引 i∈[0: j+1] 判断最小分割次数
                for i in range(j):
                    # 若 s[i+1, j+1] 为回文串
                    if dp1[i+1][j]:
                        # 则判断最小值 (是否要在 i 和 i+1 之间切一刀)
                        dp2[j] = min(dp2[i]+1, dp2[j])
                        
        return dp2[-1]

参考资料:

力扣

力扣


八、LC.139 单词拆分

8.1 题求

8.2 求解

法一:动态规划

# 36ms - 80.29%
class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> bool:
        word_set = set(wordDict)
        n = len(s)
        # dp[i] 表示 s 的前 i 位是否可被表示
        dp = [False for _ in range(n+1)]
        dp[0] = True
        
        for i in range(n):
            for j in range(i+1, n+1):
                if dp[i] and s[i:j] in word_set:
                    dp[j] = True
                    
        return dp[-1]        

参考资料:

力扣

力扣


九、LC.140 单词拆分 II

9.1 题求

9.2 求解

法一:动态规划

# 28ms - 86.81%
class Solution:
    def wordBreak(self, s: str, wordDict: List[str]) -> List[str]:
        # 单词字典 -> 单词集合
        word_set = set(wordDict)
        # 字符串长度
        n = len(s)
        # dp[j] 包含前 j 个字符 s[:j] 可构成的句子
        dp = [[] for _ in range(n+1)]
        # 前 0 个字符可构成 ""
        dp[0].append("")
        
        # 左闭边界 i
        for i in range(n):
            # 右开边界 j
            for j in range(i+1, n+1):
                # 若前 i 个字符可构成句子, 那么试探 s[i:j] 是否在单词集合中
                if dp[i] != [] and s[i:j] in word_set:
                    # 若 s[i:j] 也在单词集合中, 则前 j 个字符也可构成句子, dp[j] != []
                    # dp[j].extend([(k + " " + s[i:j]) if k else s[i:j] for k in dp[i]])
                    dp[j].extend((k + " " + s[i:j]) if k else s[i:j] for k in dp[i])
                        
        return dp[-1]

参考资料:

力扣


十、LC.152 乘积最大子数组

10.1 题求

10.2 求解

法一:动态规划 - 基本版

# 44ms - 38.56%
class Solution:
    def maxProduct(self, nums: List[int]) -> int:
        
        n = len(nums)
        # dp[k] 表示以第 k 个元素结尾的乘积最大/最小连续子数组
        dp_min = [float("inf") for _ in range(n)]
        dp_max = [float("-inf") for _ in range(n)]      
        max_product = dp_min[0] = dp_max[0] = nums[0]

        for k in range(1, n):
            product_min = dp_min[k-1] * nums[k]
            product_max = dp_max[k-1] * nums[k]
            
            dp_min[k] = min(product_min, product_max, nums[k])
            dp_max[k] = max(product_min, product_max, nums[k])
            
            max_product = max(max_product, dp_max[k])
        
        return max_product

法二:动态规划 - 降维版

# 40ms - 62.44%  
class Solution:
    def maxProduct(self, nums: List[int]) -> int:

        # 由于当前状态仅和前一状态相关, 因此可以使用滚动数组降维
        # dp_min/dap_max 表示以第 k 个元素结尾的最小/大最连续子数组乘积
        max_product = dp_min = dp_max = nums[0]

        for k in range(1, len(nums)):
            product_min = dp_min * nums[k]
            product_max = dp_max * nums[k]
            
            dp_min = min(product_min, product_max, nums[k])
            dp_max = max(product_min, product_max, nums[k])
            
            max_product = max(max_product, dp_max)
        
        return max_product

参考资料:

力扣

力扣

力扣

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值