• leetCode 718.最长重复子数组 动态规划 + 优化(滚动数组)


     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 数组

    1. class Solution {
    2. public:
    3. // 动态规划
    4. // 时间复杂度:O(n x m),n为nums1长度,m为nums2长度
    5. // 空间复杂度:O(n x m)
    6. int findLength(vector<int>& nums1, vector<int>& nums2) {
    7. vectorint>> dp(nums1.size()+1,vector<int>(nums2.size()+1,0));
    8. int result=0;
    9. for(int i=1;i<=nums1.size();i++) {
    10. for(int j=1;j<=nums2.size();j++) {
    11. if(nums1[i-1] == nums2[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
    12. if (dp[i][j] > result) result = dp[i][j];
    13. }
    14. }
    15. return result;
    16. }
    17. };
    • 时间复杂度: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的操作

    1. class Solution {
    2. public:
    3. // 优化 + 滚动数组
    4. int findLength(vector<int>& nums1, vector<int>& nums2) {
    5. vector<int> dp(vector<int>(nums2.size()+1,0));
    6. int result=0;
    7. for(int i=1;i<=nums1.size();i++) {
    8. for(int j=nums2.size();j>0;j--) {
    9. if(nums1[i-1] == nums2[j-1]) dp[j] = dp[j-1] + 1;
    10. else dp[j]=0;// 注意这里不相等的时候要有赋0的操作
    11. if (dp[j] > result) result = dp[j];
    12. }
    13. }
    14. return result;
    15. }
    16. };
    • 时间复杂度: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 的判断

    1. class Solution {
    2. public:
    3. int findLength(vector<int>& nums1, vector<int>& nums2) {
    4. vectorint>> dp (nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
    5. int result = 0;
    6. // 要对第一行,第一列经行初始化
    7. for (int i = 0; i < nums1.size(); i++) if (nums1[i] == nums2[0]) dp[i][0] = 1;
    8. for (int j = 0; j < nums2.size(); j++) if (nums1[0] == nums2[j]) dp[0][j] = 1;
    9. for (int i = 0; i < nums1.size(); i++) {
    10. for (int j = 0; j < nums2.size(); j++) {
    11. if (nums1[i] == nums2[j] && i > 0 && j > 0) { // 防止 i-1 出现负数
    12. dp[i][j] = dp[i - 1][j - 1] + 1;
    13. }
    14. if (dp[i][j] > result) result = dp[i][j];
    15. }
    16. }
    17. return result;
    18. }
    19. };

    总结:我们可以发现方案二其实是在方案一的二维dp数组的上外围和左外围多加了一层0包裹,这样做的好处是可以统一操作,简化代码,也可以更加方便的利用滚动数组进行状态压缩

    1. 方案一:
    2. if (nums1[i] == nums2[j] && i > 0 && j > 0) { // 防止 i-1 出现负数
    3. dp[i][j] = dp[i - 1][j - 1] + 1;
    4. }
    5. 方案二:
    6. if(nums1[i-1] == nums2[j-1]) dp[i][j] = dp[i-1][j-1] + 1;

    参考和推荐文章、视频:

    代码随想录 (programmercarl.com)

    动态规划之子序列问题,想清楚DP数组的定义 | LeetCode:718.最长重复子数组_哔哩哔哩_bilibili

    来自代码随想录课堂截图:

  • 相关阅读:
    Java线程池如何实现线程复用
    DETR纯代码分享(五)__init__.py(datasets)
    GDB调试方法汇总
    Nat. Med. | 基于遗传学原发部位未知癌症的分类和治疗反应预测
    二叉查找树的插入
    SpringCloud Alibaba——分布式事务 Seata
    Guava类库——Table详解
    2022年千元半入耳式蓝牙耳机,高性价比蓝牙耳机推荐
    eBay测评自养号,卖家如何运营?
    Centos7 入门
  • 原文地址:https://blog.csdn.net/weixin_41987016/article/details/133674050