- 53. Maximum Subarray">53. Maximum Subarray
- 121. Best Time to Buy and Sell Stock">121. Best Time to Buy and Sell Stock
- 5. Longest Palindromic Substring">5. Longest Palindromic Substring
- 647. Palindromic Substrings">647. Palindromic Substrings
- 300. Longest Increasing Subsequence">300. Longest Increasing Subsequence
- 42. Trapping Rain Water">42. Trapping Rain Water
- 72. Edit Distance">72. Edit Distance
- 1143. Longest Common Subsequence">1143. Longest Common Subsequence
- 718. Maximum Length of Repeated Subarray">718. Maximum Length of Repeated Subarray
- 152. Maximum Product Subarray">152. Maximum Product Subarray
- 32. Longest Valid Parentheses
*">32. Longest Valid Parentheses* - 62. Unique Paths">62. Unique Paths
- 63. Unique Paths II">63. Unique Paths II
- 221. Maximal Square">221. Maximal Square
- 剑指 Offer 19. 正则表达式匹配">剑指 Offer 19. 正则表达式匹配
- 139. Word Break">139. Word Break
- 96. Unique Binary Search Trees">96. Unique Binary Search Trees
- 股票问题
53. Maximum Subarray
class Solution:def maxSubArray(self, nums: List[int]) -> int:dp = 0 # 以nums[i]为结尾的子数组和,若为正代表可以做出正向贡献res = -10001for num in nums:dp = dp + num if dp > 0 else numres = max(res, dp)return res
- 时间复杂度:
- 空间复杂度:
Follow up **线段树**?
121. Best Time to Buy and Sell Stock
class Solution:def maxProfit(self, prices: List[int]) -> int:min_price = prices[0]profit = 0for price in prices[1:]:min_price = min(min_price, price)profit = max(profit, price - min_price)return profit
class Solution:def maxProfit(self, prices: List[int]) -> int:dp0 = 0 # 始终未持股dp1 = -prices[0] # 持股dp2 = 0 # 持股后卖出for price in prices[1:]: # 买入 and 卖出dp1, dp2 = max(dp1, dp0 - price), \max(dp2, dp1 + price)return max(dp0, dp2) # 不持股时拥有收益
- 时间复杂度:
- 空间复杂度:
5. Longest Palindromic Substring
数据范围是,暴力解法(枚举所有子串
,再判断是否是回文
)复杂度为
,显然不可行。
Approach I** 动态规划
长度为的字符串,必然是回文串;
长度为的字符串,如果两个字符相同则为回文串;
如果一个字符串(子串)本身是回文串,那么在其两端加上相同的字符,必定构成一个新的回文串。
考虑使用二维动态规划**,定义表示
的子串是否是回文串:
- 长度
和
的情况作为初始化
- 转移方程:
是
左下方的格子,故
需要逆向遍历,而
需要正向遍历
动态规划实际上是自底向上的填表过程,本题中表格为正方形,实际上利用了其右上三角部分(包括对角线)。
class Solution:def longestPalindrome(self, s: str) -> str:n = len(s)res = s[0]max_len = 1dp = [[False for _ in range(n)] for _ in range(n)]for i in range(n): # 长度为1的子串dp[i][i] = Truefor i in range(n-1, -1, -1):for j in range(i+1, n):if (cur_len := j-i+1) == 2: # 长度为2的子串dp[i][j] = s[i] == s[j]else:dp[i][j] = s[i] == s[j] and dp[i+1][j-1]if dp[i][j] and cur_len > max_len:max_len = cur_lenres = s[i:j+1]return res
- 时间复杂度:
- 空间复杂度:
Approach II** 中心扩散法
同动态规划的思考路径,考虑枚举每一个扩散位置,由中心向两边**扩散
判断是否是回文串。
- 长度为奇数的子串,中心是单个字符;
长度为偶数的子串,中心是两个字符。
class Solution:def longestPalindrome(self, s: str) -> str:def find_bound(low, high):while 0 <= low and high < len(s) and s[low] == s[high]:low -= 1high += 1return low + 1, high - 1start, end = 0, 0 # 初始状态:字符串长度至少为1for i in range(len(s)):L1, R1 = find_bound(i, i) # 从长度为1的子串开始扩散if R1 - L1 > end - start:start, end = L1, R1L2, R2 = find_bound(i, i+1) # 从长度为2的子串开始扩散if R2 - L2 > end - start:start, end = L2, R2return s[start:end+1]
时间复杂度:
- 空间复杂度:
Approach III** 马拉车Manacher算法**
暂不掌握。
647. Palindromic Substrings
class Solution:def countSubstrings(self, s: str) -> int:n = len(s)res = 0dp = [[False for _ in range(n)] for _ in range(n)]for i in range(n-1, -1, -1):for j in range(i, n):if i == j: # 子串长度==1dp[i][j] = Trueelif j == i + 1: # 子串长度==2dp[i][j] = s[i] == s[j]else: # 子串长度>=3dp[i][j] = s[i] == s[j] and dp[i+1][j-1]if dp[i][j]:res += 1return res
- 时间复杂度:
空间复杂度:
class Solution:def countSubstrings(self, s: str) -> int:def diffusion(left, right) -> int:count = 0while 0 <= left and right < n and s[left] == s[right]:left -= 1right += 1count += 1return countn = len(s)res = 0for i in range(n):res += diffusion(i, i) + diffusion(i, i+1)return res
时间复杂度:
- 空间复杂度:
300. Longest Increasing Subsequence
Approach I** 动态规划
设表示以
结尾的最长上升子序列**的长度,初始化为
。
class Solution:def lengthOfLIS(self, nums: List[int]) -> int:dp = [1] * len(nums)for i in range(1, len(nums)):for j in range(i):if nums[j] < nums[i]:dp[i] = max(dp[i], dp[j] + 1)return max(dp)
- 时间复杂度:
- 空间复杂度:
Approach I** 贪心 + 二分查找
可以更直观地考虑,不再以表示以
结尾的最长上升子序列的长度,而是让
数组直接表示我们欲求得的最长上升子序列。在遍历原数组的过程中,将每一个元素
放置到最靠左的位置,使得
数组保持严格单调递增,即让
数组的增幅达到最慢。
以上,状态定义为:表示长度为
的最长上升子序列的末尾元素的值(越小越好)。
因为所构造的数组是严格递增的(有序),故可用二分查找来求得当前元素
的插入位置。最后返回
数组有效长度**即可。
为什么不能使用单调栈?
我们的优化目标是,在遍历原数组的过程中构造最长上升子序列数组
,使得其变成更为紧凑(上升得最慢)的形式。若当前构造的数组(or单调栈)是
,欲插入的元素为
,单调栈会直接将其舍弃,而该题的二分查找会找到索引1,并将元素
替换,从而保证解题思路所定义的性质。
class Solution:def lengthOfLIS(self, nums: List[int]) -> int:dp = []for num in nums:if not dp or dp[-1] < num:dp.append(num)else:l, r = 0, len(dp) - 1while l < r:m = l + r >> 1if dp[m] < num:l = m + 1else:r = mdp[l] = numreturn len(dp)
- 时间复杂度:
- 空间复杂度:
42. Trapping Rain Water
按行求:,
是数组长度,
是数组中元素最高高度
- 设置
flag位来记录开始累计雨水的时刻,当时开始记录
按列求:
- 当前列能够累积的雨水,取决于左侧最高列与右侧最高列的最小值。
*****
Approach I** 动态规划
依然是采用按列求解的思路,不过使用额外的空间**记录每个位置的左右最高列,再通过一次遍历求得总雨水数。动态规划思想体现在左右最高列数组的构造上。
class Solution:def trap(self, heights: List[int]) -> int:n = len(heights)res = 0l_max = [0] * nfor i in range(1, n):l_max[i] = max(l_max[i-1], heights[i-1])r_max = [0] * nfor i in range(n-2, -1, -1):r_max[i] = max(r_max[i+1], heights[i+1])for i in range(1, n-1):res += max(0, min(l_max[i], r_max[i]) - heights[i])return res
- 时间复杂度:
- 空间复杂度:
Approach II** 双指针
观察上述递推式:
再考虑动态规划中的滚动数组思想,我们能否将数组优化为普通变量?由
递推得来;
由
递推得来。考虑使用双指针
与
从数组两端向中心逼近,
与
记录当前位置左右最高列高度。
从左往右遍历到位置时,
对于它而言是可信的,而
却是不可信的(
到
间可能有更高的列)。即对于该位置,左边最高列等于
,右边最高列大于或等于**
。
right_maxleft_max ____ | || |__ __?????????????????????? | |__| |__| __| |__left right
反之亦然。
,移动左指针
,移动右指针
类似思想的题目:11. Container With Most Water
class Solution:def trap(self, heights: List[int]) -> int:n = len(heights)res = 0l, r = 0, n - 1l_max = r_max = 0while l <= r:if l_max < r_max:res += max(0, l_max - heights[l])l_max = max(l_max, heights[l])l += 1else:res += max(0, r_max - heights[r])r_max = max(r_max, heights[r])r -= 1return res
- 时间复杂度:
- 空间复杂度:
Approach III** 单调栈**
class Solution:def trap(self, heights: List[int]) -> int:res = 0stack = [] # 非严格单调递减栈。存储index,为了计算Wfor i, height in enumerate(heights):while stack and heights[stack[-1]] < height:idx_down = stack.pop()if stack: # 栈为空,不能存储任何雨水W = i - stack[-1] - 1H = min(height, heights[stack[-1]]) - heights[idx_down]res += W * Hstack.append(i)return res
- 时间复杂度:
- 空间复杂度:
72. Edit Distance
Approach I** 动态规划
设表示从
转换到
需要的步骤数**。
表示
insert操作表示
delete操作表示
replace操作
添加额外的一行一列以表示 与子串互相转化的情况。

class Solution:def minDistance(self, word1: str, word2: str) -> int:m, n = len(word1), len(word2)dp = [[0 for _ in range(n+1)] for _ in range(m+1)]for i in range(1, m+1):dp[i][0] = ifor j in range(1, n+1):dp[0][j] = jfor i in range(1, m+1):for j in range(1, n+1):if word1[i-1] == word2[j-1]:dp[i][j] = dp[i-1][j-1]else:dp[i][j] = 1 + min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1])return dp[-1][-1]
- 时间复杂度:
- 空间复杂度:
Approach II** dfs**
from functools import lru_cache
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
@lru_cache(None)
def dfs(i, j):
if i == 0:
return j
if j == 0:
return i
if word1[i-1] == word2[j-1]:
return dfs(i-1, j-1)
else:
insert = dfs(i, j-1)
delete = dfs(i-1, j)
replace = dfs(i-1, j-1)
return 1 + min(insert, delete, replace)
m, n = len(word1), len(word2)
return dfs(m, n)
1143. Longest Common Subsequence
Approach I** 动态规划表示
与
的最长公共子序列**的长度。
的边界情况(即有一个子序列是
),值为
转移方程取决于
和
是否相等(见代码)
class Solution: def longestCommonSubsequence(self, text1: str, text2: str) -> int: m, n = len(text1), len(text2) dp = [[0 for _ in range(n+1)] for _ 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]: dp[i][j] = 1 + dp[i-1][j-1] else: dp[i][j] = max(dp[i-1][j], dp[i][j-1]) return dp[-1][-1]时间复杂度:
- 空间复杂度:
Approach II** dfs**
from functools import lru_cache
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
@lru_cache(None)
def dfs(i, j):
if i == 0 or j == 0:
return 0
if text1[i-1] == text2[j-1]:
return 1 + dfs(i-1, j-1)
else:
return max(dfs(i-1, j), dfs(i, j-1))
m, n = len(text1), len(text2)
return dfs(m, n)
718. Maximum Length of Repeated Subarray
:以
和
为结尾的公共子数组的最大长度。
class Solution:
def findLength(self, nums1: List[int], nums2: List[int]) -> int:
m, n = len(nums1), len(nums2)
res = 0
dp = [[0 for _ in range(n+1)] for _ in range(m+1)]
for i in range(1, m+1):
for j in range(1, n+1):
if nums1[i-1] != nums2[j-1]:
dp[i][j] = 0
else:
dp[i][j] = 1 + dp[i-1][j-1]
res = max(res, dp[i][j])
return res
- 时间复杂度:
- 空间复杂度:
class Solution:
def findLength(self, nums1: List[int], nums2: List[int]) -> int:
m, n = len(nums1), len(nums2)
res = 0
dp = [0] * (n + 1)
for i in range(1, m+1):
for j in range(n, 0, -1):
if nums1[i-1] != nums2[j-1]:
dp[j] = 0
else:
dp[j] = 1 + dp[j-1]
res = max(res, dp[j])
return res
- 时间复杂度:
- 空间复杂度:
152. Maximum Product Subarray
数组中元素满足,存在负数,故考虑增加一个状态定义:
:以
结尾的子数组的最小乘积
:以
结尾的子数组的最大乘积
只依赖于上一时刻的状态,故可进行空间优化。
实际上
dp_min与dp_max的更新需要考虑当前num的正负,但由于Python的语言特性,可以将三种情况同时写在min()和max()方法中,简化了代码。
class Solution:
def maxProduct(self, nums: List[int]) -> int:
dp_min = [11] * len(nums)
dp_max = [-11] * len(nums)
dp_min[0] = dp_max[0] = nums[0]
for i, num in enumerate(nums[1:], start=1):
dp_min[i] = min(num, num * dp_min[i-1], num * dp_max[i-1])
dp_max[i] = max(num, num * dp_min[i-1], num * dp_max[i-1])
return max(dp_max)
- 时间复杂度:
- 空间复杂度:
注意必须同时更新dp_min与dp_max,类似于梯度下降法中更新参数。
class Solution:
def maxProduct(self, nums: List[int]) -> int:
res = dp_min = dp_max = nums[0]
for i, num in enumerate(nums[1:], start=1):
dp_min, dp_max = min(num, num * dp_min, num * dp_max), \
max(num, num * dp_min, num * dp_max)
res = max(res, dp_max)
return res
- 时间复杂度:
- 空间复杂度:
32. Longest Valid Parentheses *
class Solution:
def longestValidParentheses(self, s: str) -> int:
if not s or len(s) < 2:
return 0
n = len(s)
dp = [0] * n
for i in range(1, n):
# 以'('结尾的子串有效长度必定是0
if s[i] == ')':
if s[i-1] == '(':
dp[i] = 2
if i-2 >= 0:
dp[i] += dp[i-2]
elif s[i-1] == ')':
if (j := i-1 - dp[i-1]) >= 0 and s[j] == '(':
dp[i] = 2 + dp[i-1]
if j - 1 >= 0 and s[j-1] == ')':
dp[i] += dp[j-1]
return max(dp)
- 时间复杂度:
- 空间复杂度:
62. Unique Paths
:到达位置
可能的方案数。
转移方程:,当前位置只依赖于左侧和上侧,故使用滚动数组进行空间优化。
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [1] * n
for i in range(1, m):
for j in range(1, n):
dp[j] += dp[j-1]
return dp[-1]
- 时间复杂度:
- 空间复杂度:
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
@lru_cache(None)
def dfs(i, j): # 到达位置(i, j)的路径总条数
if i == 0 or j == 0:
return 1
return dfs(i - 1, j) + dfs(i ,j - 1)
return dfs(m - 1, n - 1)
63. Unique Paths II
设有障碍物,两种方法的关键都在于初始化起点。
class Solution:
def uniquePathsWithObstacles(self, grid: List[List[int]]) -> int:
@lru_cache(None)
def dfs(i, j):
if grid[i][j] or i < 0 or j < 0:
return 0
if i == 0 and j == 0:
return 1
return dfs(i-1, j) + dfs(i, j-1)
m, n = len(grid), len(grid[0])
return dfs(m-1, n-1)
class Solution:
def uniquePathsWithObstacles(self, grid: List[List[int]]) -> int:
if grid[0][0]:
return 0
m, n = len(grid), len(grid[0])
dp = [1] + [0] * (n - 1)
for i in range(m):
for j in range(n):
if grid[i][j]:
dp[j] = 0
continue
if j > 0:
dp[j] += dp[j-1]
return dp[-1]
221. Maximal Square
:以
为右下角的正方形(只包含
1)的最大边长。
class Solution:
def maximalSquare(self, matrix: List[List[str]]) -> int:
m, n = len(matrix), len(matrix[0])
dp = [[0 for _ in range(n)] for _ in range(m)]
len_side = 0
for i in range(m):
for j in range(n):
if matrix[i][j] == '1':
if i == 0 or j == 0:
dp[i][j] = 1
else:
dp[i][j] = 1 + min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1])
len_side = max(len_side, dp[i][j])
return len_side ** 2
空间优化:注意细节
class Solution:
def maximalSquare(self, matrix: List[List[str]]) -> int:
m, n = len(matrix), len(matrix[0])
dp = [[0 for _ in range(n)] for _ in range(2)]
len_side = 0
for i in range(m):
for j in range(n):
if matrix[i][j] == '1':
if i == 0 or j == 0:
dp[i % 2][j] = 1
else:
dp[i % 2][j] = 1 + min(dp[(i-1) % 2][j-1], dp[(i-1) % 2][j], dp[i % 2][j-1])
len_side = max(len_side, dp[i % 2][j])
else:
dp[i % 2][j] = 0
return len_side ** 2
- 时间复杂度:
- 空间复杂度:
优化到
剑指 Offer 19. 正则表达式匹配
初始化!
状态转移:
class Solution: def isMatch(self, s: str, p: str) -> bool: s = ' ' + s p = ' ' + p m, n = len(s), len(p) dp = [[False for _ in range(n)] for _ in range(m)] dp[0][0] = True for j in range(2, n, 2): dp[0][j] = dp[0][j-2] and p[j] == '*' for i in range(1, m): for j in range(1, n): if p[j] == '*': dp[i][j] = dp[i][j-2] or \ dp[i-1][j] and (s[i] == p[j-1] or p[j-1] == '.') else: dp[i][j] = dp[i-1][j-1] and (s[i] == p[j] or p[j] == '.') return dp[-1][-1]
- 时间复杂度:
- 空间复杂度:
139. Word Break
表示
能否由
word_dict中的单词划分。
class Solution:
def wordBreak(self, s: str, word_dict: List[str]) -> bool:
word_dict = set(word_dict)
s = ' ' + s
n = len(s)
dp = [True] + [False] * (n - 1)
for j in range(1, n):
for i in range(max(j-19, 1), j + 1): # word最大长度是20,剪枝
if (word := s[i:j+1]) not in word_dict:
continue
if dp[i-1]:
dp[j] = True
return dp[-1]
- 时间复杂度:
,其中
恒为
- 空间复杂度:
96. Unique Binary Search Trees
股票问题
123. Best Time to Buy and Sell Stock III
当不持股时,现金最多。
return max(dp0, dp2, dp4)
对应测试用例:
同一天不能同时卖出 & 买入
class Solution:
def maxProfit(self, prices: List[int]) -> int:
dp0 = 0 # 初始状态,不持股
dp1 = -prices[0] # 第一次买入
dp2 = -inf # 第一次买入后卖出
dp3 = -inf # 第二次买入
dp4 = -inf # 第二次买入后卖出
for price in prices[1:]:
dp1, dp2, dp3, dp4 = max(dp1, dp0 - price), \
max(dp2, dp1 + price), \
max(dp3, dp2 - price), \
max(dp4, dp3 + price)
return max(dp0, dp2, dp4)
- 时间复杂度:
- 空间复杂度:
188. Best Time to Buy and Sell Stock IV
使用长度为的
数组来记录当前状态:
- 初始态;
- 买入
次,买入
次后卖出;
- ……
- 买入
次,买入
次后卖出。
使用变量diff来统一买入 or 卖出的情况。
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
if k == 0 or not prices: # 特判
return 0
k = min(k, n//2)
dp = [0] + [-prices[0]] + [-inf] * (2*k - 1)
for price in prices[1:]:
for i in range(1, 2*k + 1):
diff = -price if i & 1 else price # 奇数:买入,偶数:卖出
dp[i] = max(dp[i], dp[i-1] + diff)
return max(dp)
- 时间复杂度:
- 空间复杂度:
