好吧学会了之后并不难。
dpi的含义为以i为结尾的最长子序列长度,注意:不是0-i之间最长子序列的长度!是以i为结尾的长度!
所以设对于每一个i,j从0--i-1进行遍历,如果numsi>numsj,说明这个j结尾的最长子序列可以加一个数字了,dpi就是numsj+1,取最大,当然也可能保留上一个j的最大值,因此是:
(nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
因为dpi不是0-i之间最长子序列的长度,所以最后返回的值不是dp【len-1】,而是对于每一个i,0-len取一个最大值,所以代码是:
cpp
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if (nums.size() <= 1) return nums.size();
vector<int> dp(nums.size(), 1);
int result = 0;
for (int i = 1; i < nums.size(); i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
}
if (dp[i] > result) result = dp[i]; // 取长的子序列
}
return result;
}
};
最长重复子数组
dpij :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dpij。 (特别注意: "以下标i - 1为结尾的A" 标明一定是 以Ai-1为结尾的字符串 )
此时细心的同学应该发现,那dp00是什么含义呢?总不能是以下标-1为结尾的A数组吧。
其实dpij的定义也就决定着,我们在遍历dpij的时候i 和 j都要从1开始。
那有同学问了,我就定义dpij为 以下标i为结尾的A,和以下标j 为结尾的B,最长重复子数组长度。不行么?
行倒是行! 但实现起来就麻烦一点,需要单独处理初始化部分,在本题解下面的拓展内容里,我给出了 第二种 dp数组的定义方式所对应的代码和讲解,大家比较一下就了解了。
- 确定递推公式
根据dpij的定义,dpij的状态只能由dpi - 1j - 1推导出来。
即当Ai - 1 和Bj - 1相等的时候,dpij = dpi - 1j - 1 + 1;
根据递推公式可以看出,遍历i 和 j 要从1开始!
- dp数组如何初始化
根据dpij的定义,dpi0 和dp0j其实都是没有意义的!
但dpi0 和dp0j要初始值,因为 为了方便递归公式dpij = dpi - 1j - 1 + 1;
所以dpi0 和dp0j初始化为0。
举个例子A0如果和B0相同的话,dp11 = dp00 + 1,只有dp00初始为0,正好符合递推公式逐步累加起来。
- 确定遍历顺序
外层for循环遍历A,内层for循环遍历B。
为什么两层循环都用 <= size(),而不是第一层用 <= size1、第二层用 <= i(类似三角形遍历)?
答案是:这是最长公共子串/子序列问题,必须遍历整个二维DP表,不能用三角形遍历。
1. 为什么必须遍历整个 size1 × size2 矩形?
因为 dp[i][j] 依赖的是 dp[i-1][j-1](左上方),而不是依赖同一行或同一列的前一个值。
| j=0 | j=1 | j=2 | j=3 | |
|---|---|---|---|---|
| i=0 | 0 | 0 | 0 | 0 |
| i=1 | 0 | dp00 | dp01 | dp02 |
| i=2 | 0 | dp10 | dp11 | dp12 |
| i=3 | 0 | dp20 | dp21 | dp22 |
每个 dp[i][j] 的值只取决于 左上角 dp[i-1][j-1],也就是说:
-
计算第
i行时,需要第i-1行的所有列的数据 -
不能像三角形遍历那样只算一半,因为 对角线上的每个点都可能产生新的最长子串
2. 三角形遍历(j <= i)为什么不适用?
如果你写成:
cpp
for (int i = 1; i <= nums1.size(); i++) {
for (int j = 1; j <= i; j++) { // ❌ 三角形遍历
// ...
}
}
那么:
-
当
nums1.size() = 5, nums2.size() = 10时,你只比较了5×5/2 = 12.5个组合,漏掉了5×10 - 12.5 = 37.5个组合 -
比如
nums1[4]和nums2[9]可能相等,但j=9 > i=4,根本不会进入循环,会漏掉正确答案
因为最长公共子串不一定出现在两个数组的"前 i 个"对应位置------nums1 的第 1 个元素可以和 nums2 的第 10 个元素匹配。
3. 什么情况下用三角形遍历?
三角形遍历(j <= i)只适用于完全平方/矩阵对称问题,例如:
-
计算字符串内部的最长回文子串 (
dp[i][j]只依赖dp[i+1][j-1],且只需要上三角) -
矩阵链乘、最优二叉搜索树等区间DP
你的问题是双序列匹配 ,两个序列的长度可能不同,且匹配关系没有任何"位置对称"限制,所以必须遍历全矩形。