718. 最长重复子数组 - 力扣(LeetCode)
给两个整数数组 nums1
和 nums2
,返回 两个数组中 公共的 、长度最长的子数组的长度 。
示例 1:
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7] 输出:3 解释:长度最长的公共子数组是 [3,2,1] 。
示例 2:
输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0] 输出:5
>>思路和分析
- ① 暴力破解:先两层for循环确定两个数组起始位置,再来一个for循环或者while,来从两个起始位置开始比较,取得重复子数组的长度
- ② 动态规划:用二维数组可以记录两个字符串的所有比较情况,比较好推递推公式
用一个二维的矩阵,也就是二维的dp数组,表示这两个数组比较的所有状态。其实本题相对来说就简单很多了,因为后面的递推公式,遍历顺序,初始化,都比较简单,关键就是在于如何用dp数组去把这两个数组的比较情况,把状态保存出来。这一点是本题的难点所在。
>>动规五部曲
1.确定 dp数组 以及下标含义
- dp[i][j] : 以下标 i - 1为结尾的 nums1,和以下标 j - 1 为结尾的nums2,最长重复子数组长度为dp[i][j]
- 注意:以下标 i - 1为结尾的nums1 表明一定是以 nums[i-1]为结尾的字符串
2.确定递推公式
- 根据dp[i][j]的定义,dp[i][j]的状态只能由 dp[i-1][j-1]推导出来,即当nums1[i-1] 和 nums2[j-1] 相等时,dp[i][j] = dp[i-1][j-1] + 1;
- 思考(O_O)?:凭啥知道是依据dp[i-1][j-1] 推出 dp[i][j] ,为什么不是dp[i-1][j]或者dp[i][j-1]呢?
- 因为这里比较两个数组的元素是否相同,如果相同的话,这两个数组要一起往后退一个格,一起看后面的这个元素的状态,在后面这里一个元素的状态的基础上,再做加一。所以要看dp[i-1][j-1]。也就是说在两个数组中的元素比较完之后,应该一起回退
3.dp数组初始化
根据 dp[i][j] 的定义,可知 dp[i][0] 和 dp[0][j] 都是没有意义的!但 dp[i][0] 和 dp[0][j] 要初始值,为了方便递推公式dp[i][j] = dp[i-1][j-1] + 1;那么可将 dp[i][0] 和 dp[0][j] 初始化为0
4.确定遍历顺序 (这两种遍历方式都可以!)
- 外层 for 循环遍历 nums1,内层 for 循环遍历 nums2;
- 外层 for 循环遍历 nums2,内层 for 循环遍历 nums1;
5.举例推导 dp 数组
class Solution {
public:
// 动态规划
// 时间复杂度:O(n x m),n为nums1长度,m为nums2长度
// 空间复杂度:O(n x m)
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp(nums1.size()+1,vector<int>(nums2.size()+1,0));
int result=0;
for(int i=1;i<=nums1.size();i++) {
for(int j=1;j<=nums2.size();j++) {
if(nums1[i-1] == nums2[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
if (dp[i][j] > result) result = dp[i][j];
}
}
return result;
}
};
- 时间复杂度:O(n x m),n为nums1长度,m为nums2长度
- 空间复杂度:O(n x m)
>>优化空间复杂度「滚动数组」
- 可以看出 dp[i][j] 都是由dp[i-1][j-1]推出,那么压缩成一维数组,也就是dp[j]都是由dp[j-1]推出
- 相当于可以把上一层dp[i-1][j] 拷贝到下一层 dp[i][j] 来继续用着
注意事项
- 遍历nums2数组时,要从后向前遍历,可避免重复覆盖;
- 在不满足 nums1[i-1] == nums2[j-1] 时,注意要有赋0的操作
class Solution {
public:
// 优化 + 滚动数组
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<int> dp(vector<int>(nums2.size()+1,0));
int result=0;
for(int i=1;i<=nums1.size();i++) {
for(int j=nums2.size();j>0;j--) {
if(nums1[i-1] == nums2[j-1]) dp[j] = dp[j-1] + 1;
else dp[j]=0;// 注意这里不相等的时候要有赋0的操作
if (dp[j] > result) result = dp[j];
}
}
return result;
}
};
- 时间复杂度:O(n x m),n为nums1长度,m为nums2长度
- 空间复杂度:O(m)
拓展:若我想定义dp[i][j] 是以下标 i 为结尾的nums1,以下标 j 为结尾的 nums2 的最长重复子数组长度,可行不?
可行,只是实现相对麻烦一些。需要将第一行和第一列进行初始化
- 如果 nums1[i] 与 nums2[0] 相同的话,对应的 dp[i][0] 就要初始为1, 因为此时最长重复子数组为1
- nums2[j] 与 nums1[0] 相同的话,同理
注意事项:为了让 if (dp[i][j] > result) result = dp[i][j];
收集到全部结果,两层for训练一定从0开始遍历,这样需要加上 && i > 0 && j > 0
的判断
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
vector<vector<int>> dp (nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
int result = 0;
// 要对第一行,第一列经行初始化
for (int i = 0; i < nums1.size(); i++) if (nums1[i] == nums2[0]) dp[i][0] = 1;
for (int j = 0; j < nums2.size(); j++) if (nums1[0] == nums2[j]) dp[0][j] = 1;
for (int i = 0; i < nums1.size(); i++) {
for (int j = 0; j < nums2.size(); j++) {
if (nums1[i] == nums2[j] && i > 0 && j > 0) { // 防止 i-1 出现负数
dp[i][j] = dp[i - 1][j - 1] + 1;
}
if (dp[i][j] > result) result = dp[i][j];
}
}
return result;
}
};
总结:我们可以发现方案二其实是在方案一的二维dp数组的上外围和左外围多加了一层0包裹,这样做的好处是可以统一操作,简化代码,也可以更加方便的利用滚动数组进行状态压缩
方案一:
if (nums1[i] == nums2[j] && i > 0 && j > 0) { // 防止 i-1 出现负数
dp[i][j] = dp[i - 1][j - 1] + 1;
}
方案二:
if(nums1[i-1] == nums2[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
参考和推荐文章、视频:
代码随想录 (programmercarl.com)
动态规划之子序列问题,想清楚DP数组的定义 | LeetCode:718.最长重复子数组_哔哩哔哩_bilibili
来自代码随想录课堂截图: