当前位置: 首页 > news >正文

二维DP深度解析

二维DP深度解析

    • 一、二维DP基础认知
      • 1.1 与一维DP的核心区别
      • 1.2 核心设计步骤
    • 二、经典案例详解
      • 2.1 案例1:最长公共子序列
        • 问题描述
        • 二维DP设计
        • 代码实现
        • 复杂度分析
      • 2.2 案例2:编辑距离
        • 问题描述
        • 二维DP设计
        • 代码实现
      • 2.3 案例3:最小路径和
        • 问题描述
        • 二维DP设计
        • 代码实现
        • 空间优化:一维压缩
    • 三、二维DP的优化技巧与常见误区
      • 3.1 空间优化:从二维到一维
      • 3.2 常见误区

在动态规划(DP)的学习中,一维DP是基础,但实际问题往往需要处理更复杂的依赖关系——比如两个字符串的对比、二维网格中的路径规划等,此时二维DP(使用二维数组存储状态)成为解决问题的关键,它通过定义“二维状态”来刻画子问题的特征,能更自然地表达“两个维度的依赖关系”(如“前i个字符”与“前j个字符”的关联)。

一、二维DP基础认知

1.1 与一维DP的核心区别

  • 状态维度:一维DP用dp[i]表示“单一维度的子问题”(如“前i个元素的最优解”);二维DP用dp[i][j]表示“两个维度的子问题”(如“第一个字符串前i个字符与第二个字符串前j个字符的最优解”)。
  • 依赖关系:一维DP的dp[i]通常依赖dp[i-1]等前驱状态;二维DP的dp[i][j]可能依赖dp[i-1][j](上)、dp[i][j-1](左)、dp[i-1][j-1](左上)等多个方向的状态。
  • 适用场景:当问题涉及两个独立维度(如两个字符串、二维网格的行和列),或子问题需要同时考虑“两个变量的规模”时,二维DP更具优势。

1.2 核心设计步骤

二维DP的设计遵循DP的通用逻辑,但状态定义和递推关系更复杂:

  1. 定义二维状态:明确dp[i][j]代表的具体含义(如“字符串s1前i个字符与s2前j个字符的最长公共子序列长度”)。
  2. 推导递推关系:分析dp[i][j]dp[i-1][j]dp[i][j-1]dp[i-1][j-1]的关系(核心难点)。
  3. 确定边界条件:初始化二维数组的第一行、第一列(对应“空序列”“网格起点”等最小子问题)。
  4. 填充DP表:按一定顺序(如从左到右、从上到下)计算所有dp[i][j],最终结果通常在dp[n][m](n、m为两个维度的最大规模)。

二、经典案例详解

2.1 案例1:最长公共子序列

问题描述

给定两个字符串s1s2,找到它们最长的公共子序列(子序列可非连续)的长度。例如:

  • 输入:s1 = "abcde",s2 = "ace"
  • 输出:3(公共子序列为"ace"
二维DP设计
  1. 定义状态dp[i][j]表示“s1前i个字符(s1[0..i-1])与s2前j个字符(s2[0..j-1])的最长公共子序列长度”。
  2. 递推关系
    • s1[i-1] == s2[j-1](当前字符相同):
      这两个字符可加入公共子序列,因此dp[i][j] = dp[i-1][j-1] + 1(依赖左上状态)。
    • s1[i-1] != s2[j-1](当前字符不同):
      最长子序列只能来自“s1少一个字符”或“s2少一个字符”,取最大值:
      dp[i][j] = max(dp[i-1][j], dp[i][j-1])(依赖上、左状态)。
  3. 边界条件
    • i=0s1为空),则dp[0][j] = 0(空序列与任何序列的公共子序列长度为0)。
    • j=0s2为空),则dp[i][0] = 0(同理)。
代码实现
public class LCS {public int longestCommonSubsequence(String text1, String text2) {int n = text1.length();int m = text2.length();// 定义二维DP数组,大小(n+1) x (m+1)(0行0列用于边界)int[][] dp = new int[n + 1][m + 1];// 填充DP表:从1开始遍历(0行0列已初始化为0)for (int i = 1; i <= n; i++) {for (int j = 1; j <= m; j++) {if (text1.charAt(i - 1) == text2.charAt(j - 1)) {// 当前字符相同,取左上+1dp[i][j] = dp[i - 1][j - 1] + 1;} else {// 当前字符不同,取上或左的最大值dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);}}}return dp[n][m]; // 最终结果在右下角}public static void main(String[] args) {LCS solution = new LCS();System.out.println(solution.longestCommonSubsequence("abcde", "ace")); // 输出3}
}
复杂度分析
  • 时间复杂度O(n×m)O(n \times m)O(n×m)(两层循环遍历所有dp[i][j])。
  • 空间复杂度O(n×m)O(n \times m)O(n×m)(二维数组存储状态)。

2.2 案例2:编辑距离

问题描述

给定两个字符串s1s2,计算将s1转换为s2所需的最少操作数(可插入、删除、替换一个字符)。例如:

  • 输入:s1 = "horse",s2 = "ros"
  • 输出:3(horse → rorse(替换h→r)→ rose(删除r)→ ros(删除e))
二维DP设计
  1. 定义状态dp[i][j]表示“将s1前i个字符转换为s2前j个字符的最少操作数”。
  2. 递推关系
    • s1[i-1] == s2[j-1](当前字符相同):
      无需操作,dp[i][j] = dp[i-1][j-1](直接复用之前的结果)。
    • s1[i-1] != s2[j-1](当前字符不同):
      需从三种操作中选最少的:
      • 插入:dp[i][j-1] + 1(在s1插入s2[j-1],对应左状态+1)
      • 删除:dp[i-1][j] + 1(删除s1[i-1],对应上状态+1)
      • 替换:dp[i-1][j-1] + 1(替换s1[i-1]s2[j-1],对应左上状态+1)
        因此:dp[i][j] = min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1]) + 1
  3. 边界条件
    • dp[0][j] = js1为空,需插入j个字符才能变为s2前j个字符)。
    • dp[i][0] = is2为空,需删除i个字符才能将s1前i个字符变为空)。
代码实现
public class EditDistance {public int minDistance(String word1, String word2) {int n = word1.length();int m = word2.length();int[][] dp = new int[n + 1][m + 1];// 初始化边界:第一行(s1为空)for (int j = 0; j <= m; j++) {dp[0][j] = j;}// 初始化边界:第一列(s2为空)for (int i = 0; i <= n; i++) {dp[i][0] = i;}// 填充DP表for (int i = 1; i <= n; i++) {for (int j = 1; j <= m; j++) {if (word1.charAt(i - 1) == word2.charAt(j - 1)) {dp[i][j] = dp[i - 1][j - 1];} else {// 取三种操作的最小值+1dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]),dp[i - 1][j - 1]) + 1;}}}return dp[n][m];}public static void main(String[] args) {EditDistance solution = new EditDistance();System.out.println(solution.minDistance("horse", "ros")); // 输出3}
}

2.3 案例3:最小路径和

问题描述

给定一个包含非负整数的m x n网格,从左上角走到右下角(每次只能向下或向右移动),求路径上所有数字的最小和。例如:

  • 输入网格:
    [[1,3,1],[1,5,1],[4,2,1]]
    
  • 输出:7(路径1→3→1→1→1)
二维DP设计
  1. 定义状态dp[i][j]表示“从左上角(0,0)走到(i,j)的最小路径和”。
  2. 递推关系
    • 只能从上方(i-1,j)或左方(i,j-1)到达(i,j),因此:
      dp[i][j] = grid[i][j] + min(dp[i-1][j], dp[i][j-1])(当前网格值+前一步的最小和)。
  3. 边界条件
    • 第一行(i=0):只能从左方到达,dp[0][j] = dp[0][j-1] + grid[0][j]
    • 第一列(j=0):只能从上方到达,dp[i][0] = dp[i-1][0] + grid[i][0]
代码实现
public class MinimumPathSum {public int minPathSum(int[][] grid) {int m = grid.length;int n = grid[0].length;int[][] dp = new int[m][n];// 初始化起点dp[0][0] = grid[0][0];// 初始化第一行(只能从左走)for (int j = 1; j < n; j++) {dp[0][j] = dp[0][j - 1] + grid[0][j];}// 初始化第一列(只能从上走)for (int i = 1; i < m; i++) {dp[i][0] = dp[i - 1][0] + grid[i][0];}// 填充DP表for (int i = 1; i < m; i++) {for (int j = 1; j < n; j++) {dp[i][j] = grid[i][j] + Math.min(dp[i - 1][j], dp[i][j - 1]);}}return dp[m - 1][n - 1]; // 右下角为结果}public static void main(String[] args) {MinimumPathSum solution = new MinimumPathSum();int[][] grid = {{1, 3, 1}, {1, 5, 1}, {4, 2, 1}};System.out.println(solution.minPathSum(grid)); // 输出7}
}
空间优化:一维压缩

观察发现,dp[i][j]仅依赖dp[i-1][j](上一行)和dp[i][j-1](当前行左方),可压缩为一维数组:

public int minPathSumOptimized(int[][] grid) {int m = grid.length;int n = grid[0].length;int[] dp = new int[n]; // 仅用一行存储状态dp[0] = grid[0][0];// 初始化第一行for (int j = 1; j < n; j++) {dp[j] = dp[j - 1] + grid[0][j];}// 填充后续行for (int i = 1; i < m; i++) {dp[0] += grid[i][0]; // 第一列只能从上走for (int j = 1; j < n; j++) {// dp[j](左)和dp[j](上,未更新前即上一行的j)dp[j] = grid[i][j] + Math.min(dp[j - 1], dp[j]);}}return dp[n - 1];
}
  • 空间复杂度:从O(m×n)O(m \times n)O(m×n)降至O(n)O(n)O(n)(或O(m)O(m)O(m),取决于压缩方向)。

三、二维DP的优化技巧与常见误区

3.1 空间优化:从二维到一维

多数二维DP可通过“滚动数组”压缩空间,核心是判断状态依赖的方向:

  • dp[i][j]仅依赖dp[i-1][j](上)和dp[i][j-1](左):可压缩为一维数组(如最小路径和)。
  • dp[i][j]依赖dp[i-1][j-1](左上):需额外变量保存“左上状态”(避免被覆盖)。

例如编辑距离的空间优化(压缩为一维数组):

public int minDistanceOptimized(String word1, String word2) {int n = word1.length();int m = word2.length();int[] dp = new int[m + 1];// 初始化第一行for (int j = 0; j <= m; j++) {dp[j] = j;}for (int i = 1; i <= n; i++) {int prev = dp[0]; // 保存左上角状态(dp[i-1][j-1])dp[0] = i; // 第一列(当前行)for (int j = 1; j <= m; j++) {int temp = dp[j]; // 暂存当前值(后续作为下一轮的prev)if (word1.charAt(i - 1) == word2.charAt(j - 1)) {dp[j] = prev;} else {dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), prev) + 1;}prev = temp; // 更新prev为下一个j的左上}}return dp[m];
}

3.2 常见误区

  • 状态定义模糊:若dp[i][j]的含义不明确(如未区分“前i个”与“第i个”),会导致递推关系错误。建议在定义时明确“范围”(如“前i个字符”)。
  • 边界条件遗漏:二维DP的边界包括第一行和第一列,需单独初始化(如LCS中i=0j=0时结果为0)。
  • 忽略空间优化可行性:很多问题看似需要二维数组,但通过观察依赖关系可压缩为一维,减少内存占用(尤其对大规模输入)。

总结
二维DP通过二维状态自然表达“两个维度的子问题关系”,是解决字符串对比、网格路径等复杂问题的核心工具。其设计的关键在于:

  1. 清晰定义dp[i][j]的含义(明确“前i个”与“前j个”的子问题);
  2. 从“当前状态与周围状态的关联”推导递推关系;
  3. 初始化边界条件(第一行、第一列);
  4. 必要时通过“滚动数组”优化空间。

That’s all, thanks for reading~~
觉得有用就点个赞、收进收藏夹吧!关注我,获取更多干货~

http://www.dtcms.com/a/290797.html

相关文章:

  • Linux(Centos 7.6)命令详解:jobs
  • 服务器后台崩溃的原因
  • openpnp - 贴片前,矫正板子位置时,使用多个mark点的位置并不一定精确(mark点可能板厂做的位置就不准)
  • 小智ai MCP学习笔记
  • 2021 RoboCom 世界机器人开发者大赛-本科组(复赛)解题报告 | 珂学家
  • SOLIDWORK教育版实时协作打破空间限制
  • 组合设计模式
  • 笔试——Day14
  • tcp的三次握手与四次挥手
  • 手机录制视频时,硬编码和软编码哪个质量高?(硬件编码、软件编码)
  • 跨端分栏布局:从手机到Pad的优雅切换
  • 删除 XML 格式中双引号内的空格
  • odoo-059 xml中字段上写 domain 和 filter_domain 什么区别
  • 对理性决策模型的剖析及应用路径
  • vue2使用v-viewer图片预览:打开页面自动预览,禁止关闭预览,解决在微信浏览器的页面点击事件老是触发预览初始化的问题
  • OpenEuler 22.03 系统上安装配置gitlab runner
  • Linux网络编程-osi、udp
  • nextjs+react接口会请求两次?
  • react class和function 如何模拟vue中的 双向绑定 监听 computed的方式
  • macbookpro m1 max本儿上速搭一个elasticsearch+kibana环境
  • Electron 主进程与渲染进程之间交互方式
  • Spring Boot自动装配原理深度解析:从核心注解到实现机制
  • Electron使用WebAssembly实现CRC-16 原理校验
  • YModem在Android上的实现(四)
  • 自动化测试概念和时机
  • Java -- 自定义异常--Wrapper类--String类
  • MySQL表的基础操作
  • 第三章自定义检视面板_创建自定义编辑器类_编扩展默认组件的显示面板(本章进度6/9)
  • 奥比中光深度相机开发
  • java学习day6 + leetcode31 下一个排列