目录
扫描二维码关注公众号,回复:
14720865 查看本文章
一、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
参考资料: