今天的三道题都是要将dp[i]定义为“以数组的第i位为子序列末尾的子序列长度”。重点是该序列一定包含数组的第i位,要在此基础上来判断这一结果的来源。并且如果第i位不满足要求的话,就要将dp[1]重新设置为0或1或其他值。
第1题(LeetCode 300. 最长递增子序列)自己想到了解法。dp数组定义是这道题的关键,将dp[i]定义为“从nums开头到下标i为末尾的部分中,以nums[i]为末尾的最长递增子序列长度”。也就是说,dp[i]对应的子序列中必须包含nums[i]。而这一序列除过nums[i]的上一位置,可能是nums[i]前面的任意一个数字。所以要计算dp[i]的话,就要从0遍历到(i - 1)位置,遇到比nums[i]小的数字nums[j],就将dp[j]加上1,然后取所有dp[j] + 1(j < i)当中的最大值作为dp[i]的结果。所以状态转移方程为dp[i] = max(dp[i], dp[j] + 1)(j < i),只有当nums[j] < nums[i]时才进行这一操作。遍历方向上,外层和内层都是正向遍历。
class Solution {
public:
int lengthOfLIS(vector& nums) {
vector dp(nums.size(), 1);
dp[0] = 1;
int ans = dp[0];
for (int i = 1; i < nums.size(); ++i) {
for (int j = 0; j < i; ++j) {
if (nums[j] < nums[i]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
ans = max(ans, dp[i]);
}
return ans;
}
};
第2题(LeetCode 674. 最长连续递增序列)比较简单,相比上一题,对递增子序列加上了“连续”的限制。这样一来,dp[i]对应的序列的上一数字,只能是nums[i - 1]。否则,就从nums[i]开始重新记录子序列。所以这一题的dp数组定义仍然沿用上一题的,dp[i]定义为“从nums开头到下标i为末尾的部分中,以nums[i]为末尾的最长连续递增子序列长度”。这一定义只限制了子序列末尾必须为nums[i],而没有限制开头是哪个。
由于dp[i]只与nums[i - 1]和dp[i - 1]相关,所以不再需要内层循环,只要在外层循环正向遍历,判断前一数字与当前数字的大小关系。如果当前数字更大,就将字序列长度加1;否则,就将子序列长度重新设置为1。
class Solution {
public:
int findLengthOfLCIS(vector& nums) {
vector dp(nums.size());
dp[0] = 1;
int ans = dp[0];
for (int i = 1; i < nums.size(); ++i) {
if (nums[i] > nums[i - 1]) {
dp[i] = dp[i - 1] + 1;
}
else {
dp[i] = 1;
}
ans = max(ans, dp[i]);
}
return ans;
}
};
题解中还有贪心解法。思路就是遇到比前一数字更大的数字就将计数cnt加1,否则就将其重新设置为1,并在变力的过程中统计cnt的最大值作为最终结果。
class Solution {
public:
int findLengthOfLCIS(vector& nums) {
int ans = 1, cnt = 1;
for (int i = 1; i < nums.size(); ++i) {
if (nums[i] > nums[i - 1]) {
cnt++;
}
else {
cnt = 1;
}
ans = max(ans, cnt);
}
return ans;
}
};
第3题(LeetCode 718. 最长重复子数组)也不是很难,自己想到解法。这一题要用到二维dp数组,dp[i][j]定义为“对于nums1的[0, i]部分,nums2的[0, j]部分,两者分别以nums1[i]和nums2[j]结尾的最长重复子数组长度”。当nums1[i]与nums2[j]相等时,这一长度就在dp[i - 1][j - 1]的基础上再增加1;而不相等时,就要重新归零。
dp每个位置都依赖于其左上角的数字,所以要初始化第0行和第0列。具体的方式就是遇到nums1/nums2某一位置的数字与nums2[0]/nums1[0]想等时,就将该位置的dp值设置为1,其他位置都为0。也正因为每个位置都依赖于其左上角,所以双重循环都正向遍历。在遍历过程中,要不断更新最大值作为最终的结果。
class Solution {
public:
int findLength(vector& nums1, vector& nums2) {
vector> dp(nums1.size(), vector(nums2.size(), 0));
int ans = 0;
for (int j = 0; j < nums2.size(); ++j) {
if (nums2[j] == nums1[0]) {
dp[0][j] = 1;
ans = 1;
}
}
for (int i = 1; i < nums1.size(); ++i) {
if (nums1[i] == nums2[0]) {
dp[i][0] = 1;
ans = 1;
}
}
for (int i = 1; i < nums1.size(); ++i) {
for (int j = 1; j < nums2.size(); ++j) {
if (nums1[i] == nums2[j]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
}
else {
dp[i][j] = 0;
}
ans = max(ans, dp[i][j]);
}
}
return ans;
}
};
题解在实现上更为简洁,主要区别是在定义上将dp[i][j]对应的位置从nums1[i]和nums2[j],变为了nums1[i - 1]和nums2[j - 1]。这样一来,虽然dp矩阵的大小要设置为(nums1.size() + 1, nums2.size() + 1),但不再需要初始化nums1[0]和nums2[0]对应的第1行和第1列了。只需要将无物理含义的第0行和第0列全部初始化为0,然后从第1行、第1列开始双重循环就可以。
class Solution {
public:
int findLength(vector& nums1, vector& nums2) {
vector> dp(nums1.size() + 1, vector(nums2.size() + 1, 0));
int ans = 0;
for (int i = 1; i <= nums1.size(); ++i) {
for (int j = 1; j <= nums2.size(); ++j) {
if (nums1[i - 1] == nums2[j - 1]) { // 不再是nums1[i] == nums2[j]
dp[i][j] = dp[i - 1][j - 1] + 1;
}
else {
dp[i][j] = 0;
}
ans = max(ans, dp[i][j]);
}
}
return ans;
}
};
这种实现方法需要注意,在判断两数字是否相等的时候,要将下标从i、j改为(i - 1)、(j - 1)。
再次由于每个位置的dp值只依赖于其左上方,所以可以用节省空间的方法,将dp矩阵压缩为一维数组来实现。为避免当前行将上一行的值覆盖,导致当前位置右边的数字丢失原本左上角数字,所以内层循环要改为逆向。
class Solution {
public:
int findLength(vector& nums1, vector& nums2) {
vector dp(nums2.size() + 1, 0);
int ans = 0;
for (int i = 1; i <= nums1.size(); ++i) {
for (int j = nums2.size(); j >= 1; --j) {
if (nums1[i - 1] == nums2[j - 1]) { // 不再是nums1[i] == nums2[j]
dp[j] = dp[j - 1] + 1;
}
else {
dp[j] = 0;
}
ans = max(ans, dp[j]);
}
}
return ans;
}
};