【算法训练营Day30】动态规划part6

文章目录

最长连续递增序列

题目链接:674. 最长连续递增序列

解题思路:

使用dp数组记录当前下标的连续递增序列的长度即可

解题代码:

java 复制代码
class Solution {
    public int findLengthOfLCIS(int[] nums) {
        int[] dp = new int[nums.length];
        dp[0] = 1;
        int max = 1;
        for(int i = 1;i < nums.length;i++) {
            if(nums[i] > nums[i - 1]) dp[i] = dp[i - 1] + 1;
            else dp[i] = 1;
            if(dp[i] > max) max = dp[i];
        }

        return max;
    }
}

最长递增子序列

题目链接:300. 最长递增子序列

解题逻辑:

本题引入了子序列的概念,与上一题的区别在于:上一题是连续的递增序列,而本题是递增的子序列不一定连续。既然是要用dp,那么就要把问题拆成多个重叠的子问题,在上一题中我们是根据当前元素的前一个元素进行递推,而本题因为子序列不具有连续性,那么我们就要根据当前元素之前的递增子序列进行递推

我们从dp四部曲来分析这个问题:

  • dp数组含义:dp[i]表示以i结尾的最长递增子序列长度
  • 递推式:dp[i] = max(dp[j] + 1, dp[i])
  • 初始化:所有元素初始化为1
  • 遍历方向:从左至右

解题代码如下:

java 复制代码
class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] dp = new int[nums.length];
        for(int i = 0;i < nums.length;i++) dp[i] = 1;
        int max = 1;
        for(int i = 1;i < nums.length;i++) {
            for(int j = 0;j < i;j++) {
                if(nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                    if(dp[i] > max) max = dp[i];
                }
            }
        }
        return max;
    }
}

最长重复子数组

题目链接:718. 最长重复子数组

解题逻辑:

本题是将两个数组比较的所有状态给存储起来,然后根据状态来进行递推。

我们从dp四部曲来分析这个问题:

  • dp数组含义:dp[i][j]表示以i - 1指向的nums1数组尾部与j - 1指向的nums2数组尾部,形成的最长重复子数组长度
  • 递推式:if(nums1[i - 1] == nums2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1
  • 初始化:所有元素初始化为0
  • 遍历方向:两层for循环都是从左至右

为什么dp[i][j]表示以i - 1指向的nums1数组尾部与j - 1指向的nums2数组尾部形成的最长重复子数组长度,而不是以 i 指向的nums1数组尾部与 j 指向的nums2数组尾部形成的最长重复子数组长度?

如果是这样的话,我们的递推公式是if(nums1[i] == nums2[j]) dp[i][j] = dp[i - 1][j - 1] + 1

我们以一个4 * 4的矩阵来看的话:

我们在处理第一行以及第一列的递推时会出现越界的情况,所以这种情况下要进行一些复杂的初始化操作。那么我们的理想状态就是拥有这些阴影区域的数据,可以让我们完成第一行以及第一列的递推。那么我们干脆就把所有数据往里面缩一缩,从索引为1的行与列开始记录。如此我们的dp数组含义也要发生变化,故表示:dp[i][j]表示以i - 1指向的nums1数组尾部与j - 1指向的nums2数组尾部,形成的最长重复子数组长度

java 复制代码
class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int[][] dp = new int[nums1.length + 1][nums2.length + 1];
        int max = 0;
        for(int i = 1;i <= nums1.length;i++) {
            for(int j = 1;j <= nums2.length;j++) {
                if(nums1[i - 1] == nums2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
                if(dp[i][j] > max) max = dp[i][j];
            }
        }
        return max;
    }
}

最长公共子序列

题目链接:1143. 最长公共子序列

解题逻辑:

我们从dp四部曲来分析这个问题:

  • dp数组含义:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
  • 递推式:
    • i,j指向的字符相等:dp[i][j] = dp[i - 1][j - 1] + 1
    • i,j指向的字符不相等:dp[i][j] = Math.max(dp[i - 1][j],dp[i][j - 1]);
  • 初始化:所有元素初始化为0
  • 遍历方向:从上到下,从左到右

其实这里的递推式初次看并不好理解。要想看懂的话先要理解动态规划的本质:将子问题的结果存储到数组中,然后根据子问题的结果推导原问题的结果。当i,j指向的字符相等:dp[i][j] = dp[i - 1][j - 1] + 1。这个递推式好理解,既然i,j指向的字符相等,那我们只需要得到i - 1,j - 1对应的最长公共子序列,然后 + 1就行。但是i,j指向的字符不相等:dp[i][j] = Math.max(dp[i - 1][j],dp[i][j - 1]),这个递推式如何理解呢?既然现在i,j指向的字符不相等了,那么一起使用i与j指向的元素对最长公共子序列的值不会有增长效果,那么此时我们可以考虑继承其他情况的最长公共子序列的值。所以我们可以尝试舍弃掉i的最后一个字母,看一下i - 1,j 对应的最长公共子序列,或者舍弃掉j的最后一个字母,看一下i,j - 1对应的最长公共子序列,把他们之中较大的拿过来作为当前的最长公共子序列

解题代码:

java 复制代码
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int[][] dp = new int[text1.length() + 1][text2.length() + 1];
        int max = 0;
        for(int i = 1;i <= text1.length();i++) {
            for(int j = 1;j <= text2.length();j++) {
                if(text1.charAt(i - 1) == text2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }else {
                    dp[i][j] = Math.max(dp[i - 1][j],dp[i][j - 1]);
                }
            }
        }
        return dp[text1.length()][text2.length()];
    }
}

子数组、子序列问题中dp数组定义与返回值套路总结

问题一:什么情况下是在dp数组中寻找最大值返回,什么情况下是直接将dp数组的最后一个值返回?

这个就需要严抓dp数组的定义,例如我们可以将最长公共子序列的dp数组含义与最长重复子数组的dp数组含义做一个对比:

  • 长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
  • dp[i][j]表示以i - 1指向的nums1数组尾部与j - 1指向的nums2数组尾部,形成的最长重复子数组长度

第一个囊括了之前的所有状态,而第二个只保存了以当前字符结尾的状态,所以前面的只需要将dp数组的最后一个值返回,而第二个则需要寻找最大值。

问题二:dp[i][j]的含义什么时候代表以i-1和j-1结尾,什么时候代表[0,i-1]和[0,j-1]?

  • 定义 1:dp[i][j] 代表以 s1[i-1] 和 s2[j-1] 结尾的目标值

    • 使用场景:当问题要求子序列 / 子串必须连续 (如最长公共子串),或必须包含最后一个字符时,通常用这种定义。
    • 特点:强调结尾限制(不关注从哪开始):子序列 / 子串必须包含 s1[i-1] 和 s2[j-1] 这两个字符,且以它们为最后一个字符。
    • 核心逻辑通过强制包含结尾字符,将问题拆解为前序部分的解 + 当前字符的贡献
    • 获取结果的方式:一般通过遍历获取最大值
  • 定义 2:dp[i][j] 代表s1[0...i-1] 和 s2[0...j-1] 范围内的目标值

    • 使用场景:当问题允许子序列不连续 (如最长公共子序列),或不要求包含最后一个字符时,通常用这种定义。
    • 特点:强调范围限制:不要求包含最后一个字符,只考虑两个字符串的前 i 和前 j 个字符范围内的最优解(可能包含也可能不包含 s1[i-1] 或 s2[j-1])。
    • 核心逻辑范围性的最优解可能包含或不包含结尾字符,因此需要综合两种情况(包含 / 不包含)取最优
    • 获取结果的方式:一般直接取dp数组的末尾值即可

从正面来说这些概念不痛不痒,我们举一个反例会理解的更深刻。例如我们在最长公共子序列这个问题中,我们将dp[i][j]定义为以i - 1指向的字符串尾部与j - 1指向的字符串尾部,形成的最长公共子序列长度。那么我们随便写一个阶段的递推公式,例如:text1[2] == text2[3],那么dp[3][4] = dp[2][3] + 1; 把这个递推式翻译一下:以2结尾的text1和以3结尾的text2形成的最长公共子序列长度等于以1结尾的text1和以2结尾的text2形成的最长公共子序列长度加上1。要知道以什么结尾这种定义方式结尾是必须要包含的,那么相当于前后相连了,这其实求的是最长重复子串。

不相交的线

题目链接:1035. 不相交的线

和上一题的解题逻辑基本类似:

java 复制代码
class Solution {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        int[][] dp = new int[nums1.length + 1][nums2.length + 1];
        for(int i = 1;i <= nums1.length;i++) {
            for(int j = 1;j <= nums2.length;j++) {
                if(nums1[i - 1] == nums2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
                else dp[i][j] = Math.max(dp[i - 1][j],dp[i][j - 1]);
            }
        }
        return dp[nums1.length][nums2.length];
    }
}

最大子数组和

题目链接:53. 最大子数组和

解题逻辑:

我们从dp四部曲来分析这个问题:

  • dp数组含义:dp[i]表示以i结尾的字符串的最大子序和
  • 递推式:一个可以从dp[i - 1]这种情况(也就是不包含当前i的连续字符串的最大子序和)递推过来,也可以把当前字符当作子序头元素。哪个大选哪个:dp[i] = Math.max(dp[i - 1] + nums[i],nums[i])。
  • 初始化:dp[0]初始化为nums[0]
  • 遍历方向:从左往右遍历

解题代码:

java 复制代码
class Solution {

    public int maxSubArray(int[] nums) {
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        int max = nums[0];
        for(int i = 1;i < nums.length;i++) {
            dp[i] = Math.max(dp[i - 1] + nums[i],nums[i]);
            if(max < dp[i]) max = dp[i];
        }
        return max;
    }

}

判断子序列

题目链接:392. 判断子序列

解题逻辑:

这一题可以转化为最长公共子序列,只要最后求出的最长公共子序列的长度为s的长度,那么就说明s为t的子序列。

解题代码:

java 复制代码
class Solution {
    public boolean isSubsequence(String s, String t) {
        int[][] dp = new int[s.length() + 1][t.length() + 1];
        int max = 0;
        for(int i = 1;i <= s.length();i++) {
            for(int j = 1;j <= t.length();j++) {
                if(s.charAt(i - 1) == t.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }else {
                    dp[i][j] = Math.max(dp[i - 1][j],dp[i][j - 1]);
                }
            }
        }
        return dp[s.length()][t.length()] == s.length();
    }
}

不同的子序列

题目链接:115. 不同的子序列

解题思路:

要统计s的子序列中t出现的次数,其实可以将这个问题转化为要将s转化为t,有多少删除元素的方式。

我们从dp四部曲来分析这个问题:

  • dp数组含义:dp[i][j]表示s[0,i -1]子序列中出现t[0,j-1]的个数为dp[i][j]
  • 递推式:当s[i - 1] 与 t[j - 1]相等时,dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]。dp[i - 1][j]表示不算新添加的s[j - 1]s中t出现的次数,dp[i - 1][j - 1]表示算新添加的s[j - 1]s中t出现的次数。
  • 初始化:dp[0]初始化为nums[0]
  • 遍历方向:从左往右遍历

代码如下:

java 复制代码
class Solution {
    public int numDistinct(String s, String t) {
        int[][] dp = new int[s.length() + 1][t.length() + 1];
        for(int i = 0;i <= s.length();i++) dp[i][0] = 1;
        for(int i = 1;i <= t.length();i++) dp[0][i] = 0;
        for(int i = 1;i <= s.length();i++) {
            for(int j = 1;j <= t.length();j++) {
                if(s.charAt(i - 1) == t.charAt(j - 1)) dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1];
                else dp[i][j] = dp[i - 1][j];
            }
        }
        return dp[s.length()][t.length()];
    }
}

两个字符串的删除操作

题目链接:583. 两个字符串的删除操作

解题思路:

该问题可以直接转化为求最长公共子序列,然后根据最长公共子序列的长度可以计算出删除的最小步数

我们从dp四部曲来分析这个问题:

  • 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] + 1;
    • 不相等,dp[i][j] = max(dp[i - 1][j],dp[i][j - 1])
  • 初始化:全初始化为0
  • 遍历方向:从左往右遍历,从上往下
java 复制代码
class Solution {
    public int minDistance(String word1, String word2) {
        int[][] dp = new int[word1.length() + 1][word2.length() + 1];
        for(int i = 1;i <= word1.length();i++) {
            for(int j = 1;j <= word2.length();j++) {
                if(word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }else {
                    dp[i][j] = Math.max(dp[i - 1][j],dp[i][j - 1]);
                }
            }
        }
        int max = dp[word1.length()][word2.length()];
        return word1.length() + word2.length() - 2 * max;
    }
}

编辑距离

题目链接:72. 编辑距离

解题逻辑:

我们从dp四部曲来分析这个问题:

  • dp数组含义:dp[i][j]表示以i - 1结尾的word1转换成以j - 1结尾的word2的最小操作数
  • 递推式:
    • word1[i - 1] == word2[j - 1],说明不需要操作,dp[i][j] = dp[i - 1][j - 1];
    • 不相等,那么就会涉及到三种操作增、删、改,其中增删是一种相对操作,对word1的增,也可以通过对word2的删来实现,所以增、删考虑为一种情况。也就是说此时涉及到两种情况
      • 如果是使用增删操作,那么操作数为dp[i - 1][j] + 1、dp[i][j - 1] + 1
      • 如果是使用改操作,那么操作数为dp[i - 1][j - 1] + 1
      • 三个表达式中取最小的
  • 初始化:dp[i][0]表示i - 1结尾的word1转换成空串的步骤,所以初始化为i;dp[0][j]与上面同理。
  • 遍历方向:从左往右遍历,从上往下
java 复制代码
class Solution {
    public int minDistance(String word1, String word2) {
        int[][] dp = new int[word1.length() + 1][word2.length() + 1];
        //初始化
        for(int i = 0;i <= word1.length();i++) dp[i][0] = i;
        for(int j = 0;j <= word2.length();j++) dp[0][j] = j;
        for(int i = 1;i <= word1.length();i++) {
            for(int j = 1;j <= word2.length();j++) {
                if(word1.charAt(i - 1) == word2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1];
                else dp[i][j] = Math.min(Math.min(dp[i - 1][j] + 1,dp[i][j - 1] + 1),dp[i - 1][j - 1] + 1);
            }
        }
        return dp[word1.length()][word2.length()];
    }
}

回文子串

题目链接:647. 回文子串

解题逻辑:

我们从dp四部曲来分析这个问题:

  • dp数组含义:dp[i][j]表示s[i,j]是否为回文子串
  • 递推式:if(s[i] == s[j])
    • 情况1:i = j dp[i][j] = true;
    • 情况2:j = i + 1 dp[i][j] = true;
    • 情况3:j > i + 1 dp[i][j] = dp[i + 1][j - 1]
  • 初始化:全部初始化为false
  • 遍历方向:dp[i][j]依赖于左下角的值,所以从下往上,从左往右

解题代码:

java 复制代码
class Solution {
    public int countSubstrings(String s) {
        int n = s.length();
        boolean[][] dp = new boolean[n][n];
        int result = 0;
        for(int i = n - 1;i >= 0;i--) {
            for(int j = i;j < n;j++) {
                if(s.charAt(i) == s.charAt(j)) {
                    if(i == j) {
                        result++;
                        dp[i][j] = true;
                    }else if(i + 1 == j) {
                        result++;
                        dp[i][j] = true;
                    }else if(dp[i + 1][j - 1]){
                        result++;
                        dp[i][j] = true;
                    }
                }
            }
        }
        return result;

    }
}

最长回文子序列

题目链接:516. 最长回文子序列

解题逻辑:

我们从dp四部曲来分析这个问题:

  • dp数组含义:dp[i][j]表示s[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])。
  • 初始化:全部初始化为false
  • 遍历方向:dp[i][j]依赖于左下角的值,所以从下往上,从左往右

解题代码:

java 复制代码
class Solution {
    public int longestPalindromeSubseq(String s) {
        int n = s.length();
        int[][] dp = new int[n][n];
        for(int i = n - 1;i >= 0;i--) {
            for(int j = i;j < n;j++) {
                if(s.charAt(i) == s.charAt(j)) {
                    if(i == j) {        
                        dp[i][j] = 1;
                    }else if(i + 1 == j) {       
                        dp[i][j] = 2;
                    }else{
                        dp[i][j] = dp[i + 1][j - 1] + 2;
                    }
                }else {
                    dp[i][j] = Math.max(dp[i + 1][j],dp[i][j - 1]);
                }
            }
        }
        return dp[0][n - 1];
    }
}

最长回文子串

题目链接:5. 最长回文子串

解题逻辑:

直接在回文子串的基础上加一个结果收集比较逻辑即可

解题代码:

java 复制代码
class Solution {
    public String longestPalindrome(String s) {
        int n = s.length();
        boolean[][] dp = new boolean[n][n];
        int start = 0;
        int maxLen = 1;
        for(int i = n - 1;i >= 0;i--) {
            for(int j = i;j < n;j++) {
                if(s.charAt(i) == s.charAt(j)) {
                    if(i == j) {
                        dp[i][j] = true;
                        if(j - i + 1 >= maxLen) {
                            start = i;
                            maxLen = j - i + 1;
                        }
                    }else if(i + 1 == j) {
                        dp[i][j] = true;
                        if(j - i + 1 >= maxLen) {
                            start = i;
                            maxLen = j - i + 1;
                        }
                    }else if(dp[i + 1][j - 1]){
                        dp[i][j] = true;
                        if(j - i + 1 >= maxLen) {
                            start = i;
                            maxLen = j - i + 1;
                        }
                    }
                }
            }
        }
        return s.substring(start,start + maxLen);
    }
}
相关推荐
CoderYanger2 小时前
优选算法-双指针:2.复写零
java·后端·算法·leetcode·职场和发展
charlie1145141913 小时前
理解C++20的革命特性——协程支持2:编写简单的协程调度器
c++·学习·算法·设计模式·c++20·协程·调度器
hadage2333 小时前
--- 常见排序算法汇总 ---
算法·排序算法
Mrs.Gril3 小时前
目标检测:yolov7算法在RK3588上部署
算法·yolo·目标检测
WWZZ20255 小时前
ORB_SLAM2原理及代码解析:单应矩阵H、基础矩阵F求解
线性代数·算法·计算机视觉·机器人·slam·基础矩阵·单应矩阵
2401_841495645 小时前
【计算机视觉】分水岭实现医学诊断
图像处理·人工智能·python·算法·计算机视觉·分水岭算法·医学ct图像分割
liulilittle5 小时前
网络编程基础算法剖析:从字节序转换到CIDR掩码计算
开发语言·网络·c++·算法·通信
Kent_J_Truman6 小时前
【第几小 / 分块】
算法·蓝桥杯
搂鱼1145146 小时前
sosdp
算法