序列&编辑距离:从基础递推到跨表匹配
序列&编辑距离:从基础递推到跨表匹配
蔚蓝WEILANX哈喽,大家好呀,我是蔚蓝🥰,今天我们来深入聊聊动态规划里的「硬骨头」:序列与编辑距离系列。
不知道大家在刷 LeetCode 或者准备考研机试的时候,有没有被这几种题目绕晕过:这道题到底要不要连续?两个子序列怎么比?编辑距离那三个操作到底是啥意思?
其实,序列类 DP 就像是在玩一场「接龙游戏」。每一个状态 dp[i] 或 dp[i][j] 都是在记录一段路走完后的「当前最佳成绩」。只要理清了它是如何从之前的「小成绩」累加过来的,你会发现大厂高频题也就那么回事。
0. 序列问题的灵魂三问
在动手写代码之前,我通常会先问自己三个问题:
第一问:我的状态代表什么「快照」?
序列问题的核心通常在 「结尾」。
- 线性序列:通常定义为
dp[i]表示以第i个元素结尾的子序列性质(比如 LIS)。 - 双序列:通常定义为
dp[i][j]表示第一个序列前i个字符和第二个序列前j个字符的匹配情况。 - 区间序列:通常定义为
dp[i][j]表示区间[i, j]内的子串性质(比如回文)。
第二问:断了怎么办?(连续 vs 不连续)
这是最容易掉坑的地方。
- 连续(子数组/子串):如果当前字符不匹配,缘分就尽了,状态直接重置为 0 或者 false。
- 不连续(子序列):即使当前不匹配,之前的积累还在,你可以选择跳过当前字符,从
dp[i-1]继承大统。
第三问:我该往哪看?(遍历顺序)
- 从前往后:最常见的 LIS 和 LCS。
- 从下往上,从左往右:这是回文类区间 DP 的专属指纹,因为
dp[i][j]往往依赖它的「内心数据」dp[i+1][j-1]。
接下来的这部分,是我在备考期间整理的最全「序列与编辑距离」Java 笔记。里面包含了从最基础的长递增子序列到最复杂的编辑距离,每一道题的定义、方程、避坑点和完整源码都在这里了。
大家可以直接收藏,这套模板拿走就能用!
序列与编辑距离全集
1. 最长增子序列 (LIS)
- 300. 最长递增子序列
- 题目描述:给你一个整数数组
nums,找到其中最长严格递增子序列的长度。子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。 - dp 数组定义:
dp[i]表示以nums[i]结尾的最长递增子序列的长度。 - 状态转移方程:
dp[i] = max(dp[j] + 1),其中0 <= j < i且nums[j] < nums[i]。 - 注意点:
- 初始化:每一个
dp[i]至少为 1(自身作为一个序列)。 - 遍历顺序:两层 for 循环,外层
i从左到右,内层j遍历0到i-1。 - 最终结果:结果是整个
dp数组中的最大值,而不是最后一个元素。
- 初始化:每一个
1 | class Solution { |
2. 最长连续递增序列 (LCIS)
- 674. 最长连续递增序列
- 题目描述:给定一个未经排序的整数数组,找到最长且连续递增的子序列,并返回该序列的长度。
- dp 数组定义:
dp[i]表示以nums[i]结尾的最长连续递增子序列的长度。 - 状态转移方程:若
nums[i] > nums[i-1],则dp[i] = dp[i-1] + 1;否则dp[i] = 1。 - 注意点:
- 连续性:因为要求连续,所以只需要比较前一个元素
i-1。 - 复杂度:时间复杂度 $O(n)$。
- 连续性:因为要求连续,所以只需要比较前一个元素
1 | class Solution { |
3. 最长重复子数组
- 718. 最长重复子数组
- 题目描述:给两个整数数组
nums1和nums2,返回两个数组中公共的、长度最长的子数组(连续)的长度。 - dp 数组定义:
dp[i][j]表示以nums1[i-1]和nums2[j-1]结尾的最长公共子数组长度。 - 状态转移方程:若
nums1[i-1] == nums2[j-1],则dp[i][j] = dp[i-1][j-1] + 1;否则dp[i][j] = 0。 - 注意点:
- 数组长度:通常开
[m+1][n+1],方便处理空前缀的情况。 - 归零逻辑:子数组要求连续,如果不相等,公共前缀长度必须重置为 0。
- 最终结果:结果是整个二维数组中的最大值。
- 数组长度:通常开
1 | class Solution { |
4. 最长公共子序列 (LCS)
- 1143. 最长公共子序列
- 题目描述:给定两个字符串
text1和text2,返回这两个字符串的最长公共子序列(可不连续)的长度。 - dp 数组定义:
dp[i][j]表示text1[0..i-1]和text2[0..j-1]的最长公共子序列长度。 - 状态转移方程:
- 若
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])
- 若
- 注意点:
- 不要求连续:如果不匹配,可以从删除
text1末尾或删除text2末尾的状态中取最大值(这就是它与“子数组”的区别)。 - 最终结果:结果在右下角
dp[m][n]。
- 不要求连续:如果不匹配,可以从删除
1 | class Solution { |
5. 最大子数组和
- 53. 最大子数组和
- 题目描述:给你一个整数数组
nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 - dp 数组定义:
dp[i]表示以nums[i]结尾的连续子数组的最大和。 - 状态转移方程:
dp[i] = max(dp[i-1] + nums[i], nums[i])。 - 注意点:
- 决策意图:是把当前的
nums[i]加入到之前的子序列中,还是从当前位置自立门户。 - 空间优化:因为
dp[i]只依赖dp[i-1],可以用一个变量替代数组。
- 决策意图:是把当前的
1 | class Solution { |
6. 判断子序列
题目描述:给定字符串
s和t,判断s是否为t的子序列(不要求连续)。dp 数组定义:
dp[i][j]表示s[0..i-1]和t[0..j-1]的最长公共子序列长度。状态转移方程:同 LCS。
注意点:
- 最终判断:如果
dp[m][n] == s.length(),则说明s是t的子序列。 - 现实做法:面试中首选双指针,$O(n)$ 时间 $O(1)$ 空间。
- 最终判断:如果
1 | class Solution { |
7. 不同的子序列
题目描述:给你两个字符串
s和t,统计在s的子序列中t出现的次数。dp 数组 definition:
dp[i][j]表示s[0..i-1]的子序列中t[0..j-1]出现的个数。状态转移方程:
- 若
s[i-1] == t[j-1]:dp[i][j] = dp[i-1][j-1] + dp[i-1][j](用当前字符匹配 + 不用当前字符匹配) - 否则:
dp[i][j] = dp[i-1][j](只能选择不用当前字符匹配)
- 若
注意点:
- 初始化:
dp[i][0] = 1(空串是任何串的子序列且只有一种方案),dp[0][j] = 0(j>0)。 - 数据规模:结果可能很大,Java 中通常用
int即可,有些特殊题可能需要long或对结果取模。
- 初始化:
1 | class Solution { |
8. 两个字符串的删除操作
题目描述:给定两个单词
word1和word2,返回使得word1和word2相同所需的最小步数。每步可以删除任意一个字符串中的一个字符。dp 数组定义:
dp[i][j]表示使word1[0..i-1]和word2[0..j-1]相等所需的最小删除步数。状态转移方程:
- 若
word1[i-1] == word2[j-1]:dp[i][j] = dp[i-1][j-1] - 否则:
dp[i][j] = min(dp[i-1][j] + 1, dp[i][j-1] + 1)
- 若
注意点:
- 初始化:
dp[i][0] = i(word2 为空,删光 word1),dp[0][j] = j。 - 转化思路:最小步数 =
word1.length() + word2.length() - 2 * LCS。
- 初始化:
1 | class Solution { |
9. 编辑距离
题目描述:给你两个单词
word1和word2,请返回将word1转换成word2所使用的最少操作数(插入、删除、替换)。dp 数组定义:
dp[i][j]表示将word1[0..i-1]转换成word2[0..j-1]所使用的最少操作数。状态转移方程:
- 若
word1[i-1] == word2[j-1]:dp[i][j] = dp[i-1][j-1] - 否则:
dp[i][j] = min(dp[i-1][j-1] + 1, dp[i-1][j] + 1, dp[i][j-1] + 1)(分别是:替换、删除、插入)
- 若
注意点:
- 插入等价性:对
word1插入 ≡ 对word2删除。 - 初始化:
dp[i][0] = i,dp[0][j] = j。
- 插入等价性:对
1 | class Solution { |
10. 回文子串 (计数)
题目描述:给你一个字符串
s,请你统计并返回这个字符串中回文子串的数目。dp 数组定义:
dp[i][j]表示区间[i, j]的子串是否为回文。状态转移方程:若
s[i] == s[j]且 (j - i <= 1或dp[i+1][j-1]),则dp[i][j] = true。注意点:
- 遍历顺序:这是区间 DP,
dp[i][j]依赖左下方的dp[i+1][j-1],所以i必须从下到上,j从左到右。
- 遍历顺序:这是区间 DP,
1 | class Solution { |
11. 最长回文子序列
- 516. 最长回文子序列
- 题目描述:给你一个字符串
s,找出其中最长的回文子序列,并返回该序列的长度(子序列可不连续)。 - dp 数组定义:
dp[i][j]表示区间[i, j]内最长回文子序列的长度。 - 状态转移方程:
- 若
s[i] == s[j]:dp[i][j] = dp[i+1][j-1] + 2 - 否则:
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
- 若
- 注意点:
- 初始化:
dp[i][i] = 1。 - 遍历顺序:同样是区间 DP,
i从下到上,j从左到右。
- 初始化:
1 | class Solution { |
12. 最长回文子数组 (最长回文子串)
- 5. 最长回文子串
- 题目描述:给你一个字符串
s,找到s中最长的回文子串。 - dp 数组定义:
dp[i][j]表示区间[i, j]的子串是否为回文(boolean类型)。 - 状态转移方程:
- 若
s[i] == s[j]:- 若
j - i <= 1(长度为 1 或 2):dp[i][j] = true - 否则:
dp[i][j] = dp[i+1][j-1](取决于中间部分是否为回文)
- 若
- 否则:
dp[i][j] = false
- 若
- 注意点:
- 连续性:子数组/子串必须连续,因此不能像“子序列”那样在不相等时通过
max继承。 - 全局最值:
dp数组存储的是合法性,最长长度需要在dp[i][j] == true时通过j - i + 1动态更新。 - 遍历顺序:区间 DP 经典顺序,
i从下到上,j从左到右。
- 连续性:子数组/子串必须连续,因此不能像“子序列”那样在不相等时通过
1 | class Solution { |
碎碎念
其实写代码就像走路,最怕的就是一边走一边看导航(死记硬背方程),结果掉进坑里(理解错连续性)都不知道。
下次遇到序列问题,先别急着写代码。拿张纸画一个格子,像玩消消乐一样推演一下状态。当你能看清每一个格子里的数字是怎么滚出来的时候,那代码写起来,真的就像呼吸一样自然。
希望这份全集能帮你彻底刷通序列 DP!下次见啦~












