数据结构与算法 - 多维度DP:二维状态下的转移方程设计
本文介绍了动态规划中的二维DP问题解决方法,重点讨论了最长公共子序列(LCS)和编辑距离两个经典案例。对于LCS问题,通过定义dp[i][j]表示两个字符串前i和前j个字符的LCS长度,设计状态转移方程并在二维表中填充求解。编辑距离问题同样使用二维DP,通过比较字符是否相等来决定操作方式(插入、删除或替换),最终得到最小操作数。文章提供了Java代码实现和Mermaid流程图,直观展示了状态转移过

👋 大家好,欢迎来到我的技术博客!
💻 作为一名热爱 Java 与软件开发的程序员,我始终相信:清晰的逻辑 + 持续的积累 = 稳健的成长。
📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。
🎯 本文将围绕数据结构与算法这个话题展开,希望能为你带来一些启发或实用的参考。
🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!
文章目录
数据结构与算法 - 多维度DP:二维状态下的转移方程设计 📐📊🧩
在动态规划(Dynamic Programming, DP)的广阔世界中,我们从最基础的线性DP(如斐波那契、打家劫舍)出发,逐步深入。当问题的复杂度提升,状态不再能用一个简单的下标表示时,多维度DP 就登场了。
其中,二维DP 是最常见、最核心的多维模型。它要求我们定义一个二维的状态数组 dp[i][j],并通过精心设计的二维状态转移方程来求解问题。
今天,我们将聚焦于二维DP的核心——状态的定义与转移方程的设计。通过经典案例、Java代码和Mermaid图表,带你彻底掌握如何在二维空间中“编织”出最优解的路径。准备好了吗?让我们开始这场思维的编织之旅!🧵💻
🌱 什么是二维DP?
二维DP指的是状态需要用两个变量来描述的动态规划问题。其状态通常表示为 dp[i][j],可以理解为:
- 在一个二维网格中的位置
(i, j)。 - 两个序列/数组的前
i和前j个元素。 - 两种不同资源或属性的组合状态。
核心挑战
- 状态定义:如何用
dp[i][j]准确描述子问题? - 转移方程:
dp[i][j]如何从dp[i-1][j]、dp[i][j-1]、dp[i-1][j-1]等状态转移而来? - 边界处理:如何初始化第一行和第一列?
🧩 经典案例1:最长公共子序列(LCS)
给定两个字符串 text1 和 text2,返回它们的最长公共子序列的长度。
状态定义
dp[i][j]:字符串 text1 的前 i 个字符与 text2 的前 j 个字符的最长公共子序列长度。
状态转移方程
如果 text1[i-1] == text2[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
否则:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
边界条件
dp[0][j] = 0:空串与任何串的LCS为0。dp[i][0] = 0:同上。
Java 代码实现
public class LongestCommonSubsequence {
public static int longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length();
// dp[i][j]: text1前i个字符与text2前j个字符的LCS长度
int[][] dp = new int[m + 1][n + 1];
// 填充DP表
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
public static void main(String[] args) {
String text1 = "abcde";
String text2 = "ace";
System.out.println("LCS长度: " + longestCommonSubsequence(text1, text2)); // 输出: 3
}
}
📊 LCS状态转移过程(Mermaid)
graph TD
A[初始化 dp[0][*] = 0, dp[*][0] = 0]
B["i=1, j=1: 'a' vs 'a' → 相等 → dp[1][1] = dp[0][0] + 1 = 1"]
C["i=1, j=2: 'a' vs 'c' → 不等 → dp[1][2] = max(dp[0][2], dp[1][1]) = max(0,1) = 1"]
D["i=1, j=3: 'a' vs 'e' → 不等 → dp[1][3] = max(0,1) = 1"]
E["i=2, j=1: 'b' vs 'a' → 不等 → dp[2][1] = max(dp[1][1], dp[2][0]) = max(1,0) = 1"]
F["i=2, j=2: 'b' vs 'c' → 不等 → dp[2][2] = max(dp[1][2], dp[2][1]) = max(1,1) = 1"]
G["i=2, j=3: 'b' vs 'e' → 不等 → dp[2][3] = max(1,1) = 1"]
H["i=3, j=1: 'c' vs 'a' → 不等 → dp[3][1] = max(1,0) = 1"]
I["i=3, j=2: 'c' vs 'c' → 相等 → dp[3][2] = dp[2][1] + 1 = 1+1 = 2"]
J["i=3, j=3: 'c' vs 'e' → 不等 → dp[3][3] = max(dp[2][3], dp[3][2]) = max(1,2) = 2"]
K["i=4, j=1: 'd' vs 'a' → 不等 → dp[4][1] = max(1,0) = 1"]
L["i=4, j=2: 'd' vs 'c' → 不等 → dp[4][2] = max(1,2) = 2"]
M["i=4, j=3: 'd' vs 'e' → 不等 → dp[4][3] = max(1,2) = 2"]
N["i=5, j=1: 'e' vs 'a' → 不等 → dp[5][1] = max(1,0) = 1"]
O["i=5, j=2: 'e' vs 'c' → 不等 → dp[5][2] = max(1,2) = 2"]
P["i=5, j=3: 'e' vs 'e' → 相等 → dp[5][3] = dp[4][2] + 1 = 2+1 = 3"]
Q[最终答案: dp[5][3] = 3]
A --> B --> C --> D
D --> E --> F --> G
G --> H --> I --> J
J --> K --> L --> M
M --> N --> O --> P
P --> Q
style Q fill:#ccffcc,stroke:#6c6
✅ 可视化清晰地展示了
dp表的填充过程,最终得到 LCS 长度为 3。
🧩 经典案例2:编辑距离(Edit Distance)
给定两个单词 word1 和 word2,计算将 word1 转换为 word2 所使用的最少操作数(插入、删除、替换)。
状态定义
dp[i][j]:将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作数。
状态转移方程
如果 word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1] // 无需操作
否则:
dp[i][j] = min(
dp[i-1][j] + 1, // 删除 word1[i-1]
dp[i][j-1] + 1, // 插入 word2[j-1] 到 word1
dp[i-1][j-1] + 1 // 替换 word1[i-1] 为 word2[j-1]
)
边界条件
dp[i][0] = i:将word1前i个字符删完。dp[0][j] = j:向空串插入word2前j个字符。
Java 代码实现
public class EditDistance {
public static int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m + 1][n + 1];
// 初始化边界
for (int i = 0; i <= m; i++) dp[i][0] = i;
for (int j = 0; j <= n; j++) dp[0][j] = j;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(
Math.min(dp[i - 1][j], dp[i][j - 1]),
dp[i - 1][j - 1]
) + 1;
}
}
}
return dp[m][n];
}
}
🧩 经典案例3:二维网格中的路径问题
给定一个 m x n 的网格,机器人从左上角 (0,0) 出发,每次只能向右或向下移动,问有多少条不同的路径可以到达右下角 (m-1,n-1)。
状态定义
dp[i][j]:从 (0,0) 到 (i,j) 的不同路径数。
状态转移方程
dp[i][j] = dp[i-1][j] + dp[i][j-1]
边界条件
dp[0][j] = 1:第一行只能一直向右。dp[i][0] = 1:第一列只能一直向下。
Java 代码实现
public class UniquePaths {
public static int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
// 初始化第一行和第一列
for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n; j++) dp[0][j] = 1;
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
}
📚 推荐学习资源
- 经典题目:
- LeetCode: Longest Common Subsequence —— LCS 原题。
- LeetCode: Edit Distance —— 编辑距离原题。
- LeetCode: Unique Paths —— 网格路径问题。
- 算法教程:
- GeeksforGeeks: Dynamic Programming —— 全面的DP教程。
- Wikipedia: Dynamic Programming —— DP的百科全书式介绍。
🚨 常见误区
- 索引混淆:注意
dp[i][j]对应的是text1[i-1]和text2[j-1]。 - 边界初始化错误:忘记初始化第一行和第一列。
- 转移方程遗漏情况:如编辑距离中忘记考虑相等的情况。
- 数组大小错误:
dp数组应为(m+1) x (n+1),以包含空串情况。
🧰 调试技巧
- 打印DP表:在循环中打印
dp数组,观察填充过程。 - 小样例测试:用
2x2网格或"ab", "ac"测试。 - 手动推导:先用笔在纸上画出
dp表,再写代码。 - 单元测试:覆盖边界情况(空串、单字符)。
🌟 总结
二维DP的核心在于状态的精确定义和转移方程的逻辑推导。通过三个经典案例,我们看到:
- LCS:依赖对角线状态,用于序列比较。
- 编辑距离:综合三种操作,用于字符串相似度。
- 网格路径:依赖上方和左方状态,用于计数问题。
掌握这些模式,你就能应对大多数二维DP问题。
💬 结语
二维DP就像在一张棋盘上走格子,每一步都依赖于之前的落子。设计转移方程的过程,就是寻找这些依赖关系的艺术。
记住:状态定义是灵魂,转移方程是骨架。一旦你定义好了 dp[i][j] 的含义,转移方程往往就水到渠成了。
现在,不妨尝试分析一个新问题,问问自己:“这个问题的状态需要用两个变量来描述吗?如果是,它们分别代表什么?”
当你能清晰地回答这个问题时,二维DP的大门就已经为你敞开。🚪✨
“The best way to predict the future is to invent it.” —— 通过定义状态,我们“发明”了通往最优解的路径。
🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨
更多推荐



所有评论(0)