动态规划进阶:其他经典DP问题深度解析

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 通用解题步骤

  1. 问题分析

    • 识别问题是否适合DP
    • 分析最优子结构和重叠子问题
  2. 状态定义

    • 根据问题特点定义状态
    • 考虑状态数量和维度
  3. 状态转移

    • 推导状态转移方程
    • 考虑边界情况
  4. 初始化

    • 设置初始状态值
    • 处理特殊情况
  5. 计算顺序

    • 确定遍历顺序
    • 确保依赖状态已计算

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 优化技巧总结

  1. 空间优化

    • 滚动数组
    • 状态压缩
    • 变量复用
  2. 时间优化

    • 预处理
    • 剪枝
    • 使用更优算法(如贪心)
  3. 代码简化

    • 使用Python特性
    • 统一边界处理
    • 模块化函数

11.4 常见错误

  1. 状态定义错误

    • 状态含义不清
    • 状态维度不足
  2. 转移方程错误

    • 遗漏情况
    • 条件判断不完整
  3. 初始化错误

    • 边界值设置错误
    • 特殊情况未处理
  4. 遍历顺序错误

    • 依赖状态未计算
    • 顺序不符合问题逻辑

12. 面试准备建议

12.1 必备知识点

  1. 理解各类DP问题的特点
  2. 掌握经典问题的解法
  3. 熟悉优化技巧
  4. 了解时间空间复杂度分析

12.2 解题策略

  1. 先思考再编码:理解问题本质
  2. 从小规模开始:先解决简单情况
  3. 逐步优化:先写朴素解法,再优化
  4. 测试验证:编写测试用例

12.3 沟通表达

  1. 清晰解释思路:说明为什么用DP
  2. 展示推导过程:如何得到状态转移方程
  3. 分析复杂度:时间和空间复杂度
  4. 讨论优化:可能的优化方案

13. 练习建议

13.1 按难度练习

  1. 初级:解码方法、等差数列划分
  2. 中级:跳跃游戏、最低票价
  3. 高级:正则匹配、地下城游戏
  4. 挑战:最大矩形、套娃信封

13.2 按类型练习

  1. 字符串DP:正则、通配符、解码
  2. 游戏DP:预测赢家、跳跃游戏
  3. 几何DP:最大矩形、地下城
  4. 序列DP:套娃信封、等差数列

13.3 综合练习

  1. 一题多解:尝试不同解法
  2. 变种问题:修改条件,重新解题
  3. 实际应用:思考DP在实际中的应用

通过系统学习和大量练习,掌握这些经典DP问题的解法,能够显著提升算法解题能力。每类问题都有其独特的解题思路和技巧,需要理解本质,而不仅仅是记忆模板。

相关推荐
啊阿狸不会拉杆2 小时前
《计算机操作系统》第四章-存储器管理
人工智能·算法·计算机组成原理·os·计算机操作系统
tobias.b2 小时前
408真题解析-2010-11-数据结构-基础排序算法特征
数据结构·算法·排序算法·计算机考研·408真题解析
sali-tec2 小时前
C# 基于OpenCv的视觉工作流-章14-轮廓提取
人工智能·opencv·算法·计算机视觉
东华果汁哥2 小时前
【机器视觉 视频截帧算法】OpenCV 视频截帧算法教程
opencv·算法·音视频
我家大宝最可爱4 小时前
强化学习基础-拒绝采样
人工智能·算法·机器学习
YuTaoShao5 小时前
【LeetCode 每日一题】面试题 17.12. BiNode
算法·leetcode·深度优先
刘大猫.5 小时前
XNMS项目-拓扑图展示
java·人工智能·算法·拓扑·拓扑图·节点树·xnms
夏鹏今天学习了吗7 小时前
【LeetCode热题100(95/100)】寻找重复数
算法·leetcode·职场和发展
TTGGGFF10 小时前
控制系统建模仿真(四):线性控制系统的数学模型
人工智能·算法