import java.util.*; /** 在最长公共子序列(LCS)问题中,设计出 `dp[i][j]` 这样的二维动态规划数组,是基于对问题本质的分析和动态规划思想的自然推导。具体思路可以拆解为以下几步: ### 1. 首先理解问题的核心矛盾 LCS 问题要解决的是:**两个字符串中,找出长度最长的、顺序一致但不要求连续的公共子序列**。 例如 `str1="abcbdab"` 和 `str2="bdcaba"`,LCS 是 `"bcab"` 或 `"bdab"`,长度为 4。 关键矛盾在于:**子序列的“不连续性”和“顺序一致性”** 导致无法用简单的遍历解决,必须记录中间状态才能逐步推导。 ### 2. 从“子问题”入手(动态规划的核心思想) 动态规划的本质是**将复杂问题拆解为可重复解决的子问题**,并通过记录子问题的解来避免重复计算。 对于 LCS 问题,最自然的子问题拆分方式是: - 考虑 `str1` 的前 `i` 个字符(`str1[0..i-1]`)和 `str2` 的前 `j` 个字符(`str2[0..j-1]`) - 计算这两个“前缀子串”的 LCS 长度 为什么这样拆分?因为: - 整个问题的解(完整字符串的 LCS)可以由这些子问题的解组合而来 - 子问题之间存在重叠(例如计算 `i=3,j=4` 时可能用到 `i=2,j=3` 的结果) ### 3. 定义状态:用 `dp[i][j]` 表示子问题的解 基于上述子问题,直接定义状态: `dp[i][j]` = 字符串 `str1` 的前 `i` 个字符与 `str2` 的前 `j` 个字符的 LCS 长度。 为什么需要二维?因为子问题的边界由**两个独立变量**确定:`i`(`str1` 的前缀长度)和 `j`(`str2` 的前缀长度)。少了任何一个维度,都无法唯一确定子问题的范围。 ### 4. 推导状态转移方程(如何用子问题解求当前问题解) 确定状态后,需要找到 `dp[i][j]` 与更小的子问题(如 `dp[i-1][j]`、`dp[i][j-1]`、`dp[i-1][j-1]`)之间的关系。 分两种情况分析: - **情况1:`str1[i-1] == str2[j-1]`(当前字符相同)** 这两个字符可以直接加入 LCS 中,因此当前 LCS 长度 = 两个字符串各去掉最后一个字符后的 LCS 长度 + 1,即: `dp[i][j] = dp[i-1][j-1] + 1` - **情况2:`str1[i-1] != str2[j-1]`(当前字符不同)** 两个字符无法同时加入 LCS,因此当前 LCS 长度取决于“去掉 `str1` 最后一个字符”或“去掉 `str2` 最后一个字符”两种情况的最大值,即: `dp[i][j] = max(dp[i-1][j], dp[i][j-1])` ### 5. 确定初始状态(最小子问题的解) 当 `i=0` 或 `j=0` 时(即其中一个字符串为空),LCS 长度必然为 0。因此: `dp[0][j] = 0`(对任意 `j`) `dp[i][0] = 0`(对任意 `i`) ### 6. 最终解的位置 整个问题的解是两个完整字符串的 LCS 长度,即 `dp[str1.length][str2.length]`。 ### 总结:为什么这样设计 `dp` 数组? 1. **问题特性决定维度**:LCS 涉及两个独立字符串的匹配,必须用两个维度(`i` 和 `j`)分别表示它们的前缀范围。 2. **子问题自然拆分**:拆分前缀子串的 LCS 是最直观的子问题划分方式,能覆盖所有可能的匹配情况。 3. **状态转移逻辑自洽**:通过当前字符是否相同的两种情况,能清晰地用更小的子问题解推导出当前解。 这种设计是动态规划“拆分问题→定义状态→寻找转移关系”思想的典型应用,也是解决“两个序列匹配”类问题的通用思路(如编辑距离、最长重复子数组等均采用类似的二维 DP 设计)。 **/ // 注意类名必须为 Main, 不要有任何 package xxx 信息 public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); int len1 = in.nextInt(); int len2 = in.nextInt(); // 注意 hasNext 和 hasNextLine 的区别 String str1 = in.next(); String str2 = in.next(); char[] cs1 = str1.toCharArray(); char[] cs2 = str2.toCharArray(); // 字符串str1的前i个字符与字符串str2的前j个字符的最长公共子序列长度 int[][] dp = new int[cs1.length + 1][cs2.length + 1]; for(int i = 1; i <= cs1.length; i++){ for(int j = 1; j <= cs2.length; j++){ // 若当前字符相同 if(cs1[i - 1] == cs2[j - 1]){ dp[i][j] = dp[i - 1][j - 1] + 1; }else{ // 若当前字符不同,最长公共子序列长度取决于 “去掉 str1 最后一个字符” 或 “去掉 str2 最后一个字符” 两种情况的最大值, dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); } } } System.out.println(dp[cs1.length][cs2.length]); } }