1. 其他经典DP问题概述
其他经典DP问题涵盖了动态规划中一些独特的、难以归入前几类的经典问题。这些问题通常需要创造性的状态定义和转移方程设计。
2. 正则表达式与通配符匹配
2.1 正则表达式匹配 (LeetCode 10)
问题描述:实现支持 '.' 和 '*' 的正则表达式匹配。
状态定义
dp[i][j]:s的前i个字符与p的前j个字符是否匹配
状态转移方程
1. 如果 p[j-1] != '*':
dp[i][j] = dp[i-1][j-1] && (s[i-1] == p[j-1] || p[j-1] == '.')
2. 如果 p[j-1] == '*':
# 两种情况:
a) '*'匹配0次:dp[i][j-2]
b) '*'匹配1次或多次:dp[i-1][j] && (s[i-1] == p[j-2] || p[j-2] == '.')
Python实现
python
def isMatch(s, p):
"""
正则表达式匹配
'.' 匹配任意单个字符
'*' 匹配零个或多个前面的元素
"""
m, n = len(s), len(p)
# dp[i][j]: s前i个字符与p前j个字符是否匹配
dp = [[False] * (n + 1) for _ in range(m + 1)]
dp[0][0] = True # 空字符串匹配空模式
# 处理模式中的 '*' 可以匹配空字符串的情况
for j in range(1, n + 1):
if p[j-1] == '*':
dp[0][j] = dp[0][j-2]
for i in range(1, m + 1):
for j in range(1, n + 1):
if p[j-1] == '*':
# '*' 匹配0次
dp[i][j] = dp[i][j-2]
# '*' 匹配1次或多次
if p[j-2] == '.' or p[j-2] == s[i-1]:
dp[i][j] = dp[i][j] or dp[i-1][j]
else:
# 普通字符或 '.'
if p[j-1] == '.' or p[j-1] == s[i-1]:
dp[i][j] = dp[i-1][j-1]
return dp[m][n]
#### 递归+记忆化版本
def isMatch_memo(s, p):
memo = {}
def dfs(i, j):
if (i, j) in memo:
return memo[(i, j)]
# 模式匹配完毕
if j == len(p):
return i == len(s)
# 当前字符是否匹配
first_match = i < len(s) and (p[j] == s[i] or p[j] == '.')
# 处理 '*' 的情况
if j + 1 < len(p) and p[j+1] == '*':
# 匹配0次 或 匹配1次后继续匹配
result = dfs(i, j+2) or (first_match and dfs(i+1, j))
else:
# 普通匹配
result = first_match and dfs(i+1, j+1)
memo[(i, j)] = result
return result
return dfs(0, 0)
Java实现
java
public class RegularExpressionMatching {
public boolean isMatch(String s, String p) {
int m = s.length(), n = p.length();
boolean[][] dp = new boolean[m+1][n+1];
dp[0][0] = true;
// 初始化第一行
for (int j = 1; j <= n; j++) {
if (p.charAt(j-1) == '*') {
dp[0][j] = dp[0][j-2];
}
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (p.charAt(j-1) == '*') {
// 匹配0次
dp[i][j] = dp[i][j-2];
// 匹配1次或多次
if (p.charAt(j-2) == '.' || p.charAt(j-2) == s.charAt(i-1)) {
dp[i][j] = dp[i][j] || dp[i-1][j];
}
} else {
if (p.charAt(j-1) == '.' || p.charAt(j-1) == s.charAt(i-1)) {
dp[i][j] = dp[i-1][j-1];
}
}
}
}
return dp[m][n];
}
}
2.2 通配符匹配 (LeetCode 44)
问题描述:实现支持 '?' 和 '*' 的通配符匹配。
状态定义
dp[i][j]:s的前i个字符与p的前j个字符是否匹配
Python实现
python
def isMatch_wildcard(s, p):
"""
通配符匹配
'?' 匹配任意单个字符
'*' 匹配任意字符串(包括空字符串)
"""
m, n = len(s), len(p)
dp = [[False] * (n + 1) for _ in range(m + 1)]
dp[0][0] = True
# 处理模式中的 '*' 可以匹配空字符串的情况
for j in range(1, n + 1):
if p[j-1] == '*':
dp[0][j] = dp[0][j-1]
for i in range(1, m + 1):
for j in range(1, n + 1):
if p[j-1] == '*':
# '*' 匹配空字符串 或 匹配一个字符
dp[i][j] = dp[i][j-1] or dp[i-1][j]
elif p[j-1] == '?' or p[j-1] == s[i-1]:
dp[i][j] = dp[i-1][j-1]
return dp[m][n]
#### 双指针贪心解法(更高效)
def isMatch_wildcard_greedy(s, p):
"""
通配符匹配 - 贪心双指针
更高效的方法
"""
i = j = 0
star_idx = -1 # 记录 '*' 的位置
match = 0 # 记录匹配的位置
while i < len(s):
if j < len(p) and (p[j] == '?' or p[j] == s[i]):
# 字符匹配
i += 1
j += 1
elif j < len(p) and p[j] == '*':
# 遇到 '*',记录位置
star_idx = j
match = i
j += 1
elif star_idx != -1:
# 使用 '*' 匹配
j = star_idx + 1
match += 1
i = match
else:
return False
# 处理剩余的 '*'
while j < len(p) and p[j] == '*':
j += 1
return j == len(p)
Java实现
java
public class WildcardMatching {
// DP解法
public boolean isMatchDP(String s, String p) {
int m = s.length(), n = p.length();
boolean[][] dp = new boolean[m+1][n+1];
dp[0][0] = true;
// 处理开头的 '*'
for (int j = 1; j <= n; j++) {
if (p.charAt(j-1) == '*') {
dp[0][j] = dp[0][j-1];
}
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (p.charAt(j-1) == '*') {
dp[i][j] = dp[i][j-1] || dp[i-1][j];
} else if (p.charAt(j-1) == '?' || p.charAt(j-1) == s.charAt(i-1)) {
dp[i][j] = dp[i-1][j-1];
}
}
}
return dp[m][n];
}
// 贪心双指针解法
public boolean isMatchGreedy(String s, String p) {
int i = 0, j = 0;
int starIdx = -1, match = 0;
while (i < s.length()) {
if (j < p.length() && (p.charAt(j) == '?' || p.charAt(j) == s.charAt(i))) {
i++;
j++;
} else if (j < p.length() && p.charAt(j) == '*') {
starIdx = j;
match = i;
j++;
} else if (starIdx != -1) {
j = starIdx + 1;
match++;
i = match;
} else {
return false;
}
}
while (j < p.length() && p.charAt(j) == '*') {
j++;
}
return j == p.length();
}
}
3. 跳跃游戏系列
3.1 跳跃游戏 (LeetCode 55)
问题描述:判断是否能够从第一个位置跳到最后一个位置。
贪心解法
python
def canJump(nums):
"""
跳跃游戏 - 贪心算法
维护当前能够到达的最远位置
"""
max_reach = 0 # 当前能够到达的最远位置
for i in range(len(nums)):
if i > max_reach:
# 当前位置无法到达
return False
# 更新最远能到达的位置
max_reach = max(max_reach, i + nums[i])
if max_reach >= len(nums) - 1:
return True
return max_reach >= len(nums) - 1
#### DP解法
def canJump_DP(nums):
"""
跳跃游戏 - DP解法
dp[i]表示能否到达位置i
"""
n = len(nums)
dp = [False] * n
dp[0] = True
for i in range(n):
if dp[i]: # 只有能到达的位置才能继续跳
max_jump = min(i + nums[i], n - 1)
for j in range(i + 1, max_jump + 1):
dp[j] = True
return dp[n-1]
Java实现
java
public class JumpGame {
// 贪心解法
public boolean canJumpGreedy(int[] nums) {
int maxReach = 0;
for (int i = 0; i < nums.length; i++) {
if (i > maxReach) {
return false;
}
maxReach = Math.max(maxReach, i + nums[i]);
if (maxReach >= nums.length - 1) {
return true;
}
}
return maxReach >= nums.length - 1;
}
// DP解法
public boolean canJumpDP(int[] nums) {
int n = nums.length;
boolean[] dp = new boolean[n];
dp[0] = true;
for (int i = 0; i < n; i++) {
if (dp[i]) {
int maxJump = Math.min(i + nums[i], n - 1);
for (int j = i + 1; j <= maxJump; j++) {
dp[j] = true;
}
}
}
return dp[n-1];
}
}
3.2 跳跃游戏 II (LeetCode 45)
问题描述:使用最少的跳跃次数到达最后一个位置。
贪心解法(最优)
python
def jump(nums):
"""
跳跃游戏 II - 贪心算法
时间复杂度:O(n)
"""
n = len(nums)
if n <= 1:
return 0
jumps = 0
curr_end = 0 # 当前跳跃能够到达的最远位置
farthest = 0 # 所有选择中能够到达的最远位置
for i in range(n - 1): # 注意:不需要访问最后一个元素
farthest = max(farthest, i + nums[i])
if i == curr_end:
# 需要进行一次跳跃
jumps += 1
curr_end = farthest
if curr_end >= n - 1:
break
return jumps
#### BFS思想解法
def jump_BFS(nums):
"""
跳跃游戏 II - BFS思想
每一轮找到当前能跳到的所有位置
"""
n = len(nums)
if n <= 1:
return 0
level = 0
curr_max = 0
next_max = 0
i = 0
while curr_max >= i:
level += 1
while i <= curr_max:
next_max = max(next_max, i + nums[i])
if next_max >= n - 1:
return level
i += 1
curr_max = next_max
return 0
Java实现
java
public class JumpGameII {
public int jump(int[] nums) {
int n = nums.length;
if (n <= 1) return 0;
int jumps = 0;
int currEnd = 0;
int farthest = 0;
for (int i = 0; i < n - 1; i++) {
farthest = Math.max(farthest, i + nums[i]);
if (i == currEnd) {
jumps++;
currEnd = farthest;
if (currEnd >= n - 1) {
break;
}
}
}
return jumps;
}
}
4. 解码方法 (LeetCode 91)
问题描述:将数字字符串解码为字母(A=1, B=2, ..., Z=26),求解码方法总数。
python
def numDecodings(s):
"""
解码方法
注意:'0' 需要特殊处理
"""
if not s or s[0] == '0':
return 0
n = len(s)
dp = [0] * (n + 1)
dp[0] = 1 # 空字符串有一种解码方式
dp[1] = 1 # 第一个字符(非'0')有一种解码方式
for i in range(2, n + 1):
# 单字符解码
if s[i-1] != '0':
dp[i] += dp[i-1]
# 双字符解码
two_digit = int(s[i-2:i])
if 10 <= two_digit <= 26:
dp[i] += dp[i-2]
return dp[n]
#### 空间优化版本
def numDecodings_optimized(s):
if not s or s[0] == '0':
return 0
n = len(s)
prev2 = 1 # dp[i-2]
prev1 = 1 # dp[i-1]
for i in range(2, n + 1):
curr = 0
# 单字符解码
if s[i-1] != '0':
curr += prev1
# 双字符解码
two_digit = int(s[i-2:i])
if 10 <= two_digit <= 26:
curr += prev2
prev2, prev1 = prev1, curr
return prev1
#### 处理更复杂情况
def numDecodings_complex(s):
"""
处理 '*' 的情况(LeetCode 639)
'*' 可以代表 1-9
"""
MOD = 10**9 + 7
if not s or s[0] == '0':
return 0
n = len(s)
dp = [0] * (n + 1)
dp[0] = 1
dp[1] = 9 if s[0] == '*' else 1
for i in range(2, n + 1):
# 单字符解码
if s[i-1] == '*':
dp[i] += dp[i-1] * 9
elif s[i-1] != '0':
dp[i] += dp[i-1]
# 双字符解码
if s[i-2] == '*':
if s[i-1] == '*':
# '**' 可以表示 11-19, 21-26
dp[i] += dp[i-2] * 15 # 26-11+1 = 16, 减去20是15
elif '0' <= s[i-1] <= '6':
# '*0'~'*6' 可以表示10-16, 20-26
dp[i] += dp[i-2] * 2
else:
# '*7'~'*9' 只能表示17-19
dp[i] += dp[i-2]
elif s[i-2] == '1':
if s[i-1] == '*':
dp[i] += dp[i-2] * 9 # 11-19
else:
dp[i] += dp[i-2]
elif s[i-2] == '2':
if s[i-1] == '*':
dp[i] += dp[i-2] * 6 # 21-26
elif '0' <= s[i-1] <= '6':
dp[i] += dp[i-2]
dp[i] %= MOD
return dp[n] % MOD
Java实现
java
public class DecodeWays {
public int numDecodings(String s) {
if (s == null || s.length() == 0 || s.charAt(0) == '0') {
return 0;
}
int n = s.length();
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
// 单字符解码
if (s.charAt(i-1) != '0') {
dp[i] += dp[i-1];
}
// 双字符解码
int twoDigit = Integer.parseInt(s.substring(i-2, i));
if (twoDigit >= 10 && twoDigit <= 26) {
dp[i] += dp[i-2];
}
}
return dp[n];
}
}
5. 等差数列划分 (LeetCode 413)
问题描述:计算数组中等差子数组的数量。
python
def numberOfArithmeticSlices(nums):
"""
等差数列划分
等差子数组:长度至少为3的连续子数组
"""
n = len(nums)
if n < 3:
return 0
dp = [0] * n # dp[i]表示以nums[i]结尾的等差子数组个数
total = 0
for i in range(2, n):
if nums[i] - nums[i-1] == nums[i-1] - nums[i-2]:
dp[i] = dp[i-1] + 1
total += dp[i]
return total
#### 空间优化版本
def numberOfArithmeticSlices_optimized(nums):
n = len(nums)
if n < 3:
return 0
dp = 0 # 当前以nums[i]结尾的等差子数组个数
total = 0
for i in range(2, n):
if nums[i] - nums[i-1] == nums[i-1] - nums[i-2]:
dp += 1
total += dp
else:
dp = 0
return total
#### 数学公式解法
def numberOfArithmeticSlices_math(nums):
"""
数学公式法
连续等差数列长度为L时,等差子数组数量为(L-1)*(L-2)//2
"""
n = len(nums)
if n < 3:
return 0
total = 0
length = 2 # 当前等差数列长度
for i in range(2, n):
if nums[i] - nums[i-1] == nums[i-1] - nums[i-2]:
length += 1
else:
if length >= 3:
total += (length - 1) * (length - 2) // 2
length = 2
# 处理最后一段
if length >= 3:
total += (length - 1) * (length - 2) // 2
return total
Java实现
java
public class ArithmeticSlices {
public int numberOfArithmeticSlices(int[] nums) {
int n = nums.length;
if (n < 3) return 0;
int dp = 0;
int total = 0;
for (int i = 2; i < n; i++) {
if (nums[i] - nums[i-1] == nums[i-1] - nums[i-2]) {
dp++;
total += dp;
} else {
dp = 0;
}
}
return total;
}
}
6. 最低票价 (LeetCode 983)
问题描述:火车票有三种票价,求覆盖旅行计划的最低消费。
python
def mincostTickets(days, costs):
"""
最低票价
costs[0]: 1天票价格
costs[1]: 7天票价格
costs[2]: 30天票价格
"""
n = days[-1] # 最后一天
dp = [0] * (n + 1) # dp[i]表示到第i天的最低花费
travel_days = set(days) # 旅行的日子
for i in range(1, n + 1):
if i not in travel_days:
# 不旅行,延续前一天的花费
dp[i] = dp[i-1]
else:
# 三种选择:买1天、7天、30天票
cost1 = dp[i-1] + costs[0] # 买1天票
cost7 = dp[max(0, i-7)] + costs[1] # 买7天票
cost30 = dp[max(0, i-30)] + costs[2] # 买30天票
dp[i] = min(cost1, cost7, cost30)
return dp[n]
#### 优化:只计算旅行日
def mincostTickets_optimized(days, costs):
"""
只计算旅行日的DP
"""
n = len(days)
dp = [float('inf')] * n
durations = [1, 7, 30] # 票的有效期
for i in range(n):
for cost, duration in zip(costs, durations):
# 找到第一张覆盖不到当前旅行的票
j = i
while j >= 0 and days[i] - days[j] < duration:
j -= 1
prev_cost = dp[j] if j >= 0 else 0
dp[i] = min(dp[i], prev_cost + cost)
return dp[n-1]
Java实现
java
public class MinimumCostForTickets {
public int mincostTickets(int[] days, int[] costs) {
int lastDay = days[days.length - 1];
int[] dp = new int[lastDay + 1];
boolean[] travel = new boolean[lastDay + 1];
for (int day : days) {
travel[day] = true;
}
for (int i = 1; i <= lastDay; i++) {
if (!travel[i]) {
dp[i] = dp[i-1];
} else {
int cost1 = dp[i-1] + costs[0];
int cost7 = dp[Math.max(0, i-7)] + costs[1];
int cost30 = dp[Math.max(0, i-30)] + costs[2];
dp[i] = Math.min(cost1, Math.min(cost7, cost30));
}
}
return dp[lastDay];
}
}
7. 地下城游戏 (LeetCode 174)
问题描述:骑士从左上角到右下角救公主,每个格子有正负血量,求最小初始血量。
python
def calculateMinimumHP(dungeon):
"""
地下城游戏 - 逆向DP
从终点逆向计算到起点
"""
m, n = len(dungeon), len(dungeon[0])
# dp[i][j]表示从(i,j)到终点所需的最小初始血量
dp = [[float('inf')] * (n + 1) for _ in range(m + 1)]
# 终点右侧和下侧初始化为1(最少需要1点血活着)
dp[m][n-1] = dp[m-1][n] = 1
for i in range(m-1, -1, -1):
for j in range(n-1, -1, -1):
# 需要的最小血量 = 右边或下边需要的最小血量 - 当前格子值
# 但不能小于1(至少要有1点血活着)
need = min(dp[i+1][j], dp[i][j+1]) - dungeon[i][j]
dp[i][j] = max(1, need)
return dp[0][0]
#### 空间优化版本
def calculateMinimumHP_optimized(dungeon):
m, n = len(dungeon), len(dungeon[0])
# 使用一维数组
dp = [float('inf')] * (n + 1)
dp[n-1] = 1 # 终点需要的最小血量
for i in range(m-1, -1, -1):
# 从右向左更新
for j in range(n-1, -1, -1):
need = min(dp[j], dp[j+1]) - dungeon[i][j]
dp[j] = max(1, need)
return dp[0]
Java实现
java
public class DungeonGame {
public int calculateMinimumHP(int[][] dungeon) {
int m = dungeon.length, n = dungeon[0].length;
int[][] dp = new int[m+1][n+1];
// 初始化边界
for (int i = 0; i <= m; i++) {
Arrays.fill(dp[i], Integer.MAX_VALUE);
}
dp[m][n-1] = dp[m-1][n] = 1;
for (int i = m-1; i >= 0; i--) {
for (int j = n-1; j >= 0; j--) {
int need = Math.min(dp[i+1][j], dp[i][j+1]) - dungeon[i][j];
dp[i][j] = Math.max(1, need);
}
}
return dp[0][0];
}
}
8. 俄罗斯套娃信封问题 (LeetCode 354)
问题描述:二维的最长递增子序列问题。
python
def maxEnvelopes(envelopes):
"""
俄罗斯套娃信封问题
先按宽度升序,宽度相同按高度降序
然后在高度上求LIS
"""
if not envelopes:
return 0
# 排序:宽度升序,宽度相同则高度降序
envelopes.sort(key=lambda x: (x[0], -x[1]))
# 在高度上求LIS
heights = [h for _, h in envelopes]
return lengthOfLIS_binary(heights)
def lengthOfLIS_binary(nums):
"""
最长递增子序列 - 贪心+二分
"""
tails = []
for num in nums:
# 二分查找插入位置
left, right = 0, len(tails)
while left < right:
mid = (left + right) // 2
if tails[mid] < num:
left = mid + 1
else:
right = mid
if left == len(tails):
tails.append(num)
else:
tails[left] = num
return len(tails)
#### 动态规划解法
def maxEnvelopes_DP(envelopes):
"""
DP解法 - O(n²)
"""
if not envelopes:
return 0
envelopes.sort(key=lambda x: (x[0], x[1]))
n = len(envelopes)
dp = [1] * n
for i in range(n):
for j in range(i):
if (envelopes[j][0] < envelopes[i][0] and
envelopes[j][1] < envelopes[i][1]):
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
Java实现
java
public class RussianDollEnvelopes {
public int maxEnvelopes(int[][] envelopes) {
if (envelopes == null || envelopes.length == 0) {
return 0;
}
// 排序:宽度升序,宽度相同高度降序
Arrays.sort(envelopes, (a, b) -> {
if (a[0] == b[0]) {
return b[1] - a[1];
}
return a[0] - b[0];
});
// 在高度上求LIS
int[] heights = new int[envelopes.length];
for (int i = 0; i < envelopes.length; i++) {
heights[i] = envelopes[i][1];
}
return lengthOfLIS(heights);
}
private int lengthOfLIS(int[] nums) {
int[] tails = new int[nums.length];
int size = 0;
for (int num : nums) {
int left = 0, right = size;
while (left < right) {
int mid = left + (right - left) / 2;
if (tails[mid] < num) {
left = mid + 1;
} else {
right = mid;
}
}
tails[left] = num;
if (left == size) {
size++;
}
}
return size;
}
}
9. 预测赢家 (LeetCode 486)
问题描述:两人轮流从数组两端取数,预测先手是否能赢。
python
def PredictTheWinner(nums):
"""
预测赢家 - 区间DP
dp[i][j]表示在区间[i,j]内,先手能比后手多得的分数
"""
n = len(nums)
dp = [[0] * n for _ in range(n)]
# 初始化:单个数字,先手获得该数字
for i in range(n):
dp[i][i] = nums[i]
# 按区间长度遍历
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
# 先手可以选择i或j,然后变成后手
dp[i][j] = max(nums[i] - dp[i+1][j],
nums[j] - dp[i][j-1])
return dp[0][n-1] >= 0
#### 递归+记忆化
def PredictTheWinner_memo(nums):
memo = {}
def dfs(i, j):
if i == j:
return nums[i]
if (i, j) in memo:
return memo[(i, j)]
# 先手选择i或j,然后变成后手
score_i = nums[i] - dfs(i+1, j)
score_j = nums[j] - dfs(i, j-1)
result = max(score_i, score_j)
memo[(i, j)] = result
return result
return dfs(0, len(nums)-1) >= 0
#### 空间优化
def PredictTheWinner_optimized(nums):
n = len(nums)
dp = nums[:] # 初始化
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
dp[i] = max(nums[i] - dp[i+1],
nums[j] - dp[i])
return dp[0] >= 0
Java实现
java
public class PredictTheWinner {
public boolean predictTheWinner(int[] nums) {
int n = nums.length;
int[][] dp = new int[n][n];
// 初始化
for (int i = 0; i < n; i++) {
dp[i][i] = nums[i];
}
// 按区间长度遍历
for (int len = 2; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
dp[i][j] = Math.max(
nums[i] - dp[i+1][j],
nums[j] - dp[i][j-1]
);
}
}
return dp[0][n-1] >= 0;
}
}
10. 最大矩形 (LeetCode 85)
问题描述:在二进制矩阵中找到最大的只包含1的矩形。
python
def maximalRectangle(matrix):
"""
最大矩形 - 使用柱状图最大矩形算法
"""
if not matrix or not matrix[0]:
return 0
m, n = len(matrix), len(matrix[0])
heights = [0] * (n + 1) # 多加一个0,方便处理
max_area = 0
for i in range(m):
# 更新每一行的高度
for j in range(n):
if matrix[i][j] == '1':
heights[j] += 1
else:
heights[j] = 0
# 计算当前行的最大矩形面积
stack = []
for j in range(n + 1):
while stack and heights[j] < heights[stack[-1]]:
height = heights[stack.pop()]
width = j if not stack else j - stack[-1] - 1
max_area = max(max_area, height * width)
stack.append(j)
return max_area
#### 动态规划解法
def maximalRectangle_DP(matrix):
if not matrix or not matrix[0]:
return 0
m, n = len(matrix), len(matrix[0])
# left[i][j]: 位置(i,j)左边连续1的个数
left = [[0] * n for _ in range(m)]
max_area = 0
for i in range(m):
for j in range(n):
if matrix[i][j] == '1':
# 计算左边连续1的个数
left[i][j] = left[i][j-1] + 1 if j > 0 else 1
# 向上扩展,计算最大矩形
width = left[i][j]
for k in range(i, -1, -1):
width = min(width, left[k][j])
if width == 0:
break
height = i - k + 1
max_area = max(max_area, width * height)
return max_area
Java实现
java
public class MaximalRectangle {
public int maximalRectangle(char[][] matrix) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return 0;
}
int m = matrix.length, n = matrix[0].length;
int[] heights = new int[n + 1]; // 多加一个0
int maxArea = 0;
for (int i = 0; i < m; i++) {
// 更新高度
for (int j = 0; j < n; j++) {
if (matrix[i][j] == '1') {
heights[j]++;
} else {
heights[j] = 0;
}
}
// 计算当前行的最大矩形
Stack<Integer> stack = new Stack<>();
for (int j = 0; j <= n; j++) {
while (!stack.isEmpty() && heights[j] < heights[stack.peek()]) {
int height = heights[stack.pop()];
int width = stack.isEmpty() ? j : j - stack.peek() - 1;
maxArea = Math.max(maxArea, height * width);
}
stack.push(j);
}
}
return maxArea;
}
}
11. 解题模板总结
11.1 通用解题步骤
-
问题分析:
- 识别问题是否适合DP
- 分析最优子结构和重叠子问题
-
状态定义:
- 根据问题特点定义状态
- 考虑状态数量和维度
-
状态转移:
- 推导状态转移方程
- 考虑边界情况
-
初始化:
- 设置初始状态值
- 处理特殊情况
-
计算顺序:
- 确定遍历顺序
- 确保依赖状态已计算
11.2 常见问题模式
| 问题类型 | 关键技巧 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 正则匹配 | 二维DP,处理'*'和'.' | O(mn) | O(mn) |
| 通配符 | 二维DP,'*'匹配任意 | O(mn) | O(mn) |
| 跳跃游戏 | 贪心,维护最远距离 | O(n) | O(1) |
| 解码方法 | 一维DP,处理'0' | O(n) | O(n) |
| 等差数列 | 一维DP,记录连续长度 | O(n) | O(n) |
| 地下城 | 逆向DP,从终点开始 | O(mn) | O(mn) |
| 套娃信封 | 排序+LIS | O(nlogn) | O(n) |
| 预测赢家 | 区间DP,博弈 | O(n²) | O(n²) |
| 最大矩形 | 柱状图+单调栈 | O(mn) | O(n) |
11.3 优化技巧总结
-
空间优化:
- 滚动数组
- 状态压缩
- 变量复用
-
时间优化:
- 预处理
- 剪枝
- 使用更优算法(如贪心)
-
代码简化:
- 使用Python特性
- 统一边界处理
- 模块化函数
11.4 常见错误
-
状态定义错误:
- 状态含义不清
- 状态维度不足
-
转移方程错误:
- 遗漏情况
- 条件判断不完整
-
初始化错误:
- 边界值设置错误
- 特殊情况未处理
-
遍历顺序错误:
- 依赖状态未计算
- 顺序不符合问题逻辑
12. 面试准备建议
12.1 必备知识点
- 理解各类DP问题的特点
- 掌握经典问题的解法
- 熟悉优化技巧
- 了解时间空间复杂度分析
12.2 解题策略
- 先思考再编码:理解问题本质
- 从小规模开始:先解决简单情况
- 逐步优化:先写朴素解法,再优化
- 测试验证:编写测试用例
12.3 沟通表达
- 清晰解释思路:说明为什么用DP
- 展示推导过程:如何得到状态转移方程
- 分析复杂度:时间和空间复杂度
- 讨论优化:可能的优化方案
13. 练习建议
13.1 按难度练习
- 初级:解码方法、等差数列划分
- 中级:跳跃游戏、最低票价
- 高级:正则匹配、地下城游戏
- 挑战:最大矩形、套娃信封
13.2 按类型练习
- 字符串DP:正则、通配符、解码
- 游戏DP:预测赢家、跳跃游戏
- 几何DP:最大矩形、地下城
- 序列DP:套娃信封、等差数列
13.3 综合练习
- 一题多解:尝试不同解法
- 变种问题:修改条件,重新解题
- 实际应用:思考DP在实际中的应用
通过系统学习和大量练习,掌握这些经典DP问题的解法,能够显著提升算法解题能力。每类问题都有其独特的解题思路和技巧,需要理解本质,而不仅仅是记忆模板。