8.动态规划:序列DP问题(记忆化搜索=>递推,LIS模型和LCS模型、打家劫舍问题)【灵神基础精讲】

来源0x3f:https://space.bilibili.com/206214

文章目录

总结:枚举选哪个 vs 选或不选

动态规划,什么时候用枚举选哪个?什么时候用选或不选?

  • 看数据范围,当 n = 1 0 5 n = 10^5 n=105 时,使用枚举选哪个会超时,因为状态个数 1 0 5 10^5 105 + 状态转移 1 0 5 10^5 105
  • 很多时候,问题描述:你可以从当前位置i,移动到满足 i < j 的任意位置 +【条件一、条件二】,不需要知道精确的信息nums[i],只需要抽象问题的要求。

枚举选哪个:适用于需要完全知道子序列相邻两数的信息。

  • 如最长递增子序列( O ( n 2 ) O(n^2) O(n2)

选或不选:适用于 ① 子序列相邻数字无关、② 子序列相邻数字弱关联(奇偶性或只需要知道 0 和 1的信息)


给大家分享一下自己对边界条件的总结🤗

不同的题目,所要的答案不同, 比如:方案数,最大、小值,数字个数,能否构成?

这也就意味着 dp 数组值可以为数值,也可以是 boolean 类型

另外,同样是数值的情况下,不同的要求,也会造成不同的初始值 f[0][0]

  • 能否构成:f[0][0] = True ; 0 可以构成 0

  • 方案数:f[0][0] = 1 ;0 组成 0 只有一种方案

  • 数字个数: f[0][0] = 0; 0 组成 0 没有使用数字

  • 最大、小值: 问题一般会回归到 方案数 或 数字个数问题, 一般会使用到 max/min 函数约束答案,而且会使用 ±inf 初始化来表示极端情况。 比如:力扣 279 求最小数量

一、动态规划(回溯=>记忆化搜索=>动态规划)

状态定义?状态转移方程?

启发思路:选或不选 / 选哪个


DP萌新三步:

1、思考回溯要怎么写

入参和返回值;递归到哪里;递归边界和入口

2、改成记忆化搜索

3、1:1翻译成递推

198. 打家劫舍

难度中等2445

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 400

题解:从右往左思考,如果选择第i个房子,那么i-1的房子不能选(直接跳到i-2),如果不选第i个房子,那么能选i-1的房子(递归到i-1)

回溯 + 记忆化搜索

class Solution {
    Map<Integer, Integer> map = new HashMap<>();
    public int rob(int[] nums) {
        return dfs(nums, nums.length-1);
    }

    public int dfs(int[] nums, int i){
        if(i < 0){
            return 0;
        }
        if(map.containsKey(i)) return map.get(i);
        // 表示不选i 和 选i
        int res = Math.max(dfs(nums, i-1), dfs(nums, i-2) + nums[i]);
        map.put(i, res);
        return res;
    }
}

动态规划

class Solution {
    // 递推dp[i] = max(dp[i-2]+nums[i-2], dp[i-1])
    // 左右i同时+2 = > 
    public int rob(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n+2];
        for(int i = 0; i < n; i++){
            dp[i+2] = Math.max(dp[i+1], dp[i] + nums[i]);
        }
        return dp[n+1];
    }
}

空间优化:滚动数组

class Solution {
    // 递推dp[i] = max(dp[i-2]+nums[i-2], dp[i-1])
    // 左右i同时+2 = > 
    public int rob(int[] nums) {
        int n = nums.length;
        int f0 = 0, f1 = 0; // f1 上一次结果 f0 上上次结果
        for(int i = 0; i < n; i++){
            int newf = Math.max(f1, f0 + nums[i]);
            f0 = f1;
            f1 = newf;
        }
        return f1;
    }
}

70. 爬楼梯

难度简单2873

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:

输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

示例 2:

输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶

提示:

  • 1 <= n <= 45
class Solution {
    public int climbStairs(int n) {
        int[] dp = new int[n+5];
        dp[1] = 1;
        dp[2] = 2;
        for(int i = 3; i <= n; i++){
            dp[i] = dp[i-1] + dp[i-2];
        }
        return dp[n];
    }
}

/** 回溯
class Solution {
    int res = 0;
    public int climbStairs(int n) {
        return dfs(n, 0);
    }

    public int dfs(int n , int i){
        if(i > n) return 0;
        if(i == n){
            return 1;
        }
        return dfs(n, i+1) + dfs(n, i+2);
    }
}
 */

213. 打家劫舍 II

难度中等1275

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

示例 1:

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 3:

输入:nums = [1,2,3]
输出:3

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 1000

题解:

对于一个数组,成环的话主要有如下三种情况:

  • 情况一:考虑不包含首尾元素
  • 情况二:考虑包含首元素,不包含尾元素
  • 情况三:考虑包含尾元素,不包含首元素
  • 情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了
class Solution {
    public int rob(int[] nums) {
        if(nums.length == 0) return 0;
        else if(nums.length == 1) return nums[0];
        int count1 = rob198(nums, 0 , nums.length-1);
        int count2 = rob198(nums, 1 , nums.length);
        return Math.max(count1, count2);
    }

    public int rob198(int[] nums, int start, int end) {
        int[] dp = new int[end+2];
        for(int i = start; i < end; i++){
            dp[i+2] = Math.max(dp[i+1], dp[i] + nums[i]);
        }
        return dp[end+1];
    }
}

二、两个字符串DP模型(LCS、删除 插入 替换等操作使两个字符串相同)

核心在于 四种情况:i、j分别选还是不选

1143. 最长公共子序列

难度中等1232

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例 1:

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。

示例 2:

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。

示例 3:

输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。

提示:

  • 1 <= text1.length, text2.length <= 1000
  • text1text2 仅由小写英文字符组成。

记忆化搜索

class Solution {
    char[] arr1,arr2;
    int[][] cache;
    public int longestCommonSubsequence(String text1, String text2) {
        arr1 = text1.toCharArray();
        arr2 = text2.toCharArray();
        int n = text1.length(), m = text2.length();
        cache = new int[n+1][m+1];
        for(int i = 0; i <= n; i++) Arrays.fill(cache[i], -1);
        return dfs(n-1, m-1);
    }

    public int dfs(int i, int j){
        if(i < 0 || j < 0) return 0;
        if(cache[i][j] != -1) return cache[i][j];
        if(arr1[i] == arr2[j]) return cache[i][j] = dfs(i-1, j-1) + 1;
        return cache[i][j] = Math.max(dfs(i-1, j), dfs(i, j-1));
    }
}

记忆化搜索改递推:

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int n = text1.length(), m = text2.length();
        int[][] dp = new int[n + 1][m + 1];
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; 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[n][m];
    }
}

72. 编辑距离

难度困难2823

给你两个单词 word1word2请返回将 word1 转换成 word2 所使用的最少操作数

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

示例 1:

输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

示例 2:

输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')

提示:

  • 0 <= word1.length, word2.length <= 500
  • word1word2 由小写英文字母组成

记忆化搜索

class Solution {
    char[] s,t;
    int[][] cache;
    public int minDistance(String word1, String word2) {
        s = word1.toCharArray();
        t = word2.toCharArray();
        int n = s.length, m = t.length;
        cache = new int[n][m];
        for(int i = 0; i < n; i++){
            Arrays.fill(cache[i], -1); // -1表示没有访问过
        }
        return dfs(n-1, m-1);
    }

    public int dfs(int i, int j){
        if(i < 0 || j < 0){
            // 下标为 0~j 的字符串长度为 j+1
            return i < 0 ? j+1 : i+1;
        }
        if(cache[i][j] != -1) return cache[i][j];
        if(s[i] == t[j]) return cache[i][j] = dfs(i-1, j-1); 
        //dfs(i,j-1) : 插入 ; dfs(i-1,j):删除 ; dfs(i-1, j-1) : 替换
        return cache[i][j] = Math.min(dfs(i,j-1), Math.min(dfs(i-1,j), dfs(i-1, j-1))) + 1;
    }
}

递推

class Solution {
    public int minDistance(String word1, String word2) {
        int n = word1.length(), m = word2.length();
        if(n == 0) return m;
        if(m == 0) return n;
        int[][] dp = new int[n+1][m+1];
        for(int i = 0; i <= n; i++) dp[i][0] = i;
        for(int j = 0; j <= m; j++) dp[0][j] = j;
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++){
                if(word1.charAt(i-1) == word2.charAt(j-1)){
                    dp[i][j] = dp[i-1][j-1];
                }else{
                    dp[i][j] = Math.min(dp[i-1][j], Math.min(dp[i][j-1], dp[i-1][j-1])) + 1;
                }
            }
        }
        return dp[n][m];
    }
}

583. 两个字符串的删除操作

难度中等542

给定两个单词 word1word2 ,返回使得 word1word2 相同所需的最小步数

每步 可以删除任意一个字符串中的一个字符。

示例 1:

输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"

示例 2:

输入:word1 = "leetcode", word2 = "etco"
输出:4

提示:

  • 1 <= word1.length, word2.length <= 500
  • word1word2 只包含小写英文字母
class Solution {
    char[] arr1, arr2;
    int[][] cache;
    public int minDistance(String word1, String word2) {
        arr1 = word1.toCharArray();
        arr2 = word2.toCharArray();
        int n = word1.length(), m = word2.length();
        cache = new int[n+1][m+1];
        for(int i = 0; i <= n; i++) Arrays.fill(cache[i], -1);
        return dfs(n-1, m-1);
    }

    public int dfs(int i, int j){
        if(i < 0 || j < 0){
            return i < 0 ? j+1 : i+1;
        }
        if(cache[i][j] != -1) return cache[i][j];
        if(arr1[i] == arr2[j]) return cache[i][j] = dfs(i-1, j-1);
        else{
            return cache[i][j] = Math.min(dfs(i-1, j), dfs(i, j-1)) + 1;
        }
    }
}

记忆化搜索转递推

class Solution {
    public int minDistance(String word1, String word2) {
        int n = word1.length(), m = word2.length();
        int[][] dp = new int[n+1][m+1];
        for(int i = 0; i <= n; i++) dp[i][0] = i;
        for(int j = 0; j <= m; j++) dp[0][j] = j; 
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++){
                if(word1.charAt(i-1) == word2.charAt(j-1))
                    dp[i][j] = dp[i-1][j-1];
                else{
                    dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + 1;
                }
            }
        }
        return dp[n][m];
    }
}

712. 两个字符串的最小ASCII删除和

难度中等310

给定两个字符串s1s2,返回 使两个字符串相等所需删除字符的 ASCII 值的最小和

示例 1:

输入: s1 = "sea", s2 = "eat"
输出: 231
解释: 在 "sea" 中删除 "s" 并将 "s" 的值(115)加入总和。
在 "eat" 中删除 "t" 并将 116 加入总和。
结束时,两个字符串相等,115 + 116 = 231 就是符合条件的最小和。

示例 2:

输入: s1 = "delete", s2 = "leet"
输出: 403
解释: 在 "delete" 中删除 "dee" 字符串变成 "let",
将 100[d]+101[e]+101[e] 加入总和。在 "leet" 中删除 "e" 将 101[e] 加入总和。
结束时,两个字符串都等于 "let",结果即为 100+101+101+101 = 403 。
如果改为将两个字符串转换为 "lee" 或 "eet",我们会得到 433 或 417 的结果,比答案更大。

提示:

  • 0 <= s1.length, s2.length <= 1000
  • s1s2 由小写英文字母组成

记忆化搜索

class Solution {
    char[] arr1, arr2;
    int[][] cache;
    public int minimumDeleteSum(String word1, String word2) {
        arr1 = word1.toCharArray();
        arr2 = word2.toCharArray();
        int n = word1.length(), m = word2.length();
        cache = new int[n+1][m+1];
        for(int i = 0; i <= n; i++) Arrays.fill(cache[i], -1);
        return dfs(n-1, m-1);
    }

    public int dfs(int i, int j){
        if(i < 0){
            int rest = 0;
            while(j >= 0){
                rest += arr2[j];
                j--;
            }
            return rest;
        }
        if(j < 0){
            int rest = 0;
            while(i >= 0){
                rest += arr1[i];
                i--;
            }
            return rest;
        }
        if(cache[i][j] != -1) return cache[i][j];
        if(arr1[i] == arr2[j]){
            return cache[i][j] = dfs(i-1, j-1);
        }
        else{
            int cost = Integer.MAX_VALUE;
            cost = Math.min(dfs(i-1, j) + arr1[i], cost);
            cost = Math.min(dfs(i, j-1) + arr2[j], cost); 
            return cache[i][j] = cost;
        }
    }
}

记忆化搜索改成递推

class Solution {
    public int minimumDeleteSum(String word1, String word2) {
        char[] arr1 = word1.toCharArray();
        char[] arr2 = word2.toCharArray();
        int n = word1.length(), m = word2.length();
        int[][] dp = new int[n+1][m+1];
        for(int i = 1; i <= n; i++) dp[i][0] = dp[i-1][0] + arr1[i-1];
        for(int j = 1; j <= m; j++) dp[0][j] = dp[0][j-1] + arr2[j-1];
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++){
                if(arr1[i-1] == arr2[j-1]){
                    dp[i][j] = dp[i-1][j-1];
                }else{
                    dp[i][j] = Math.min(dp[i-1][j] + arr1[i-1], dp[i][j-1] + arr2[j-1]);
                }
            }
        }
        return dp[n][m];
    }
}

1458. 两个子序列的最大点积

难度困难76

给你两个数组 nums1nums2

请你返回 nums1nums2 中两个长度相同的 非空 子序列的最大点积。

数组的非空子序列是通过删除原数组中某些元素(可能一个也不删除)后剩余数字组成的序列,但不能改变数字间相对顺序。比方说,[2,3,5][1,2,3,4,5] 的一个子序列而 [1,5,3] 不是。

示例 1:

输入:nums1 = [2,1,-2,5], nums2 = [3,0,-6]
输出:18
解释:从 nums1 中得到子序列 [2,-2] ,从 nums2 中得到子序列 [3,-6] 。
它们的点积为 (2*3 + (-2)*(-6)) = 18 。

示例 2:

输入:nums1 = [3,-2], nums2 = [2,-6,7]
输出:21
解释:从 nums1 中得到子序列 [3] ,从 nums2 中得到子序列 [7] 。
它们的点积为 (3*7) = 21 。

示例 3:

输入:nums1 = [-1,-1], nums2 = [1,1]
输出:-1
解释:从 nums1 中得到子序列 [-1] ,从 nums2 中得到子序列 [1] 。
它们的点积为 -1 。

提示:

  • 1 <= nums1.length, nums2.length <= 500
  • -1000 <= nums1[i], nums2[i] <= 100
class Solution {
    int[] nums1, nums2;
    int[][] cache;
    public int maxDotProduct(int[] nums1, int[] nums2) {
        this.nums1 = nums1; this.nums2 = nums2;
        int n = nums1.length, m = nums2.length;
        cache = new int[n+1][m+1];
        for(int i = 0; i <= n; i++) Arrays.fill(cache[i], Integer.MIN_VALUE);
        return dfs(n-1, m-1);
    }

    public int dfs(int i, int j){
        if(i < 0 || j < 0) 
            return Integer.MIN_VALUE / 2; // 防止下面溢出
        if(cache[i][j] != Integer.MIN_VALUE) 
            return cache[i][j];
        int res = nums1[i] * nums2[j]; // 单选i和j
        res = Math.max(res, dfs(i-1, j-1) + nums1[i] * nums2[j]); // 选i,j这个组合 + 前面的序列组合
        res = Math.max(res, dfs(i-1, j)); // 不选i 和 不选j 的情况
        res = Math.max(res, dfs(i, j-1));
        return cache[i][j] = res;
    }
}

记忆化搜索转成动态规划

class Solution {
    public int maxDotProduct(int[] nums1, int[] nums2) {
        int n = nums1.length, m = nums2.length;
        int[][] dp = new int[n+1][m+1];
        for(int i = 0; i <= n; i++) dp[i][0] = Integer.MIN_VALUE / 2;
        for(int j = 0; j <= m; j++) dp[0][j] = Integer.MIN_VALUE / 2;
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= m; j++){
                dp[i][j] = Math.max(
                        Math.max(nums1[i-1] * nums2[j-1], dp[i-1][j-1] + nums1[i-1] * nums2[j-1]),
                        Math.max(dp[i-1][j], dp[i][j-1]));
            }
        }
        return dp[n][m];
    }
}

三、一个字符串DP模型(LIS)

300. 最长递增子序列

难度中等3041

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

输入:nums = [0,1,0,3,2,3]
输出:4

示例 3:

输入:nums = [7,7,7,7,7,7,7]
输出:1

提示:

  • 1 <= nums.length <= 2500
  • -104 <= nums[i] <= 104

O(n)动态规划

  • 定义dfs(i)表示以nums[i]结尾的LIS长度

记忆化搜索

class Solution {
    int[] nums;
    int[] cache;
    public int lengthOfLIS(int[] nums) {
        this.nums = nums;
        int n = nums.length;
        int res = 0;
        cache = new int[n+1];
       	// 每位i都可能是LIS的结尾,因此要枚举每一个i作为结尾进行dfs
        for(int i = n-1; i >= 0; i--){
            res = Math.max(res, dfs(i));
        }
        return res;
    }

    public int dfs(int i){
        if(i < 0) return 0;
        if(cache[i] != 0) return cache[i];
        int res = 0;
        for(int j = i-1; j >= 0; j--){
            if(nums[j] < nums[i]){
                res = Math.max(res, dfs(j));
            }
        }
        return cache[i] = res + 1;
    }
}

记忆化搜索转递推

class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n+1];
        int res = 0;
        for(int i = 0; i < n; i++){
            for(int j = i-1; j >= 0; j--){
                if(nums[j] < nums[i]){
                    dp[i] = Math.max(dp[i], dp[j]);
                }
            }
            dp[i] = dp[i] + 1;
            res = Math.max(dp[i], res);
        }
        return res;
    }
}
// ##################################################
// 注意上面的最后写法:dp[i] = dp[i] + 1; 是为了节省初始化数组fill(1) 的时间
// 因此在[0,i-1] 最长递增子序列长度上加上自己,就是dp[i] = dp[i] + 1
// 如果 [0,i-1] 中没有满足条件的dp[j],则此时dp[i]=1,相当于初始化了
// 如果想要在遍历中写成dp[i] = Math.max(dp[i], dp[j] + 1),则需要先初始化
class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n+1];
        Arrays.fill(dp, 1);
        int res = 0;
        for(int i = 0; i < n; i++){
            for(int j = i-1; j >= 0; j--){
                if(nums[j] < nums[i]){
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            res = Math.max(dp[i], res);
        }
        return res;
    }
}

时间复杂度如何进一步优化?

进阶技巧: 交换状态与状态值

dp[i] : 表示末尾元素为 nums[i] 的LIS长度

== > g[i] : 表示长度为 i+1 的LIS 的末尾元素的最小值

O(nlogn)贪心 + 二分查找解法

  • 贪心 + 二分解法实际上增加了LIS长度变大的可能性
class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        // g[i] : 表示长度为 i+1 的LIS 的末尾元素的最小值
        int[] g = new int[n];
        int res = 0; // len
        for(int num : nums){
            int i = 0, j = res; // [i, j)
            while(i < j){
                int m = (i + j) / 2;
                if(g[m] < num) i = m+1;
                else j = m;
            }
            g[i] = num;
            if(res == j) res++;
        }
        return res;
    }
}

写法二:

class Solution {
    public int lengthOfLIS(int[] nums) {
        // O(nlogn)
        List<Integer> g = new ArrayList<>();
        for(int x : nums){
            int pos = lower_bound(g, x);
            if(pos == g.size()){
                g.add(x); // >= x 的 g[i] 不存在
            }else
                g.set(pos, x);
        }
        return g.size();
    }

    // 找出第一个大于等于 x 的下标
    public int lower_bound(List<Integer> g, int x){
        int left = 0, right = g.size();
        while(left < right){
            int mid = (left + right) >> 1;
            if(g.get(mid) < x) left = mid + 1;
            else right = mid;
        }
        return right;
    }
}

线段树解法

使用滚动数组优化,可以去掉第一个条件,等价于单点修改,区间查询的问题

  • 等号左侧:单点修改
  • 等号右侧:区间求max

===>线段树

具体来说,定义 f [ i ] [ j ] f[i][j] f[i][j] 表示 nums \textit{nums} nums 的前 i i i个元素中,以元素 j j j 结尾的满足条件的子序列的最长长度。

  • j ≠ nums [ i ] j\ne\textit{nums}[i] j=nums[i] 时, f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j] = f[i-1][j] f[i][j]=f[i1][j]
  • j = nums [ i ] j=\textit{nums}[i] j=nums[i] 时,我们可以从 f [ i − 1 ] [ j ′ ] f[i-1][j'] f[i1][j] 转移过来,这里 j ′ < j j'<j j<j取最大值,得 f [ i ] [ j ] = 1 + max ⁡ j ′ = 0 j − 1 f [ i − 1 ] [ j ′ ] f[i][j] = 1 + \max_{j'=0}^{j-1} f[i-1][j'] f[i][j]=1+maxj=0j1f[i1][j]

上式有一个「区间求最大值」的过程,这非常适合用线段树计算,且由于 f [ i ] f[i] f[i] 只会从 f [ i − 1 ] f[i-1] f[i1] 转移过来,我们可以把 f f f 的第一个维度优化掉。这样我们可以用线段树表示整个 j j j 数组,在上面查询和更新。

最后答案为 max ⁡ ( f [ n − 1 ] ) \max(f[n-1]) max(f[n1]),对应到线段树上就是根节点的值。

class Solution {
    public int lengthOfLIS(int[] nums) {
        // 离散化
        Set<Integer> set = new HashSet<>(); // 防止重复
        for(int x : nums) set.add(x);
        List<Integer> copy = new ArrayList<>(set);
        Collections.sort(copy);

        N = copy.size();
        int ans = 0;
        for(int num : nums){
            // 二分找到 >= nums 的第一个下标, 查找的元素一定存在
            int left = 0, right = copy.size();
            while(left < right){
                int mid = (left + right) >> 1;
                if(copy.get(mid) < num) left = mid + 1;
                else right = mid;
            }
            int k = right; // num 对应的下标 k
            // 查找以元素值(1,num-1)结尾的LIS的最大值
            int cnt = 1 + query(root,0,N,0,k-1);
            // 更新 值[k]的最大值
            // 注意这里是覆盖更新,对应的模版中覆盖更新不需要累加
            update(root,0,N,k,k,cnt);
            ans = Math.max(ans, cnt);
        }
        return ans;
    }
    
    class Node {
        // 左右孩子节点
        Node left, right;
        // 当前节点值,以及懒惰标记的值
        int val, add;
    }
    private int N = (int) 1e9;
    private Node root = new Node();
    public void update(Node node, int start, int end, int l, int r, int val) {
        if (l <= start && end <= r) {
            node.val = val;
            node.add = val;
            return;
        }
        pushDown(node);
        int mid = (start + end) >> 1;
        if (l <= mid) update(node.left, start, mid, l, r, val);
        if (r > mid) update(node.right, mid + 1, end, l, r, val);
        pushUp(node);
    }
    public int query(Node node, int start, int end, int l, int r) {
        if (r < start || end < l) return 0;
        if (l <= start && end <= r) return node.val;
        pushDown(node);
        int mid = (start + end) >> 1, ans = 0;
        if (l <= mid) ans = query(node.left, start, mid, l, r);
        if (r > mid) ans = Math.max(ans, query(node.right, mid + 1, end, l, r));
        return ans;
    }
    private void pushUp(Node node) {
        // 每个节点存的是当前区间的最大值
        node.val = Math.max(node.left.val, node.right.val);
    }
    private void pushDown(Node node) {
        if (node.left == null) node.left = new Node();
        if (node.right == null) node.right = new Node();
        if (node.add == 0) return;
        node.left.val = node.add;
        node.right.val = node.add;
        node.left.add = node.add;
        node.right.add = node.add;
        node.add = 0;
    }
}

树状数组解法



class Solution {
    /**
    1. 先对nums节点离散化
    2. 每次都找比当前数小的最长递增序列,不断更新结果
     */
    public int lengthOfLIS(int[] nums) {
        // 离散化
        Set<Integer> set = new HashSet<>(); // 防止重复
        for(int x : nums) set.add(x);
        List<Integer> copy = new ArrayList<>(set);
        Collections.sort(copy);

        int res = 0;
        // 值域树状数组,用树状数组维护以元素值j结尾的最长子序列长度
        BIT tree = new BIT(copy.size()+1);
    for(int num : nums){
        // 二分找到 >= nums 的第一个下标, 查找的元素一定存在
        int left = 0, right = copy.size();
        while(left < right){
            int mid = (left + right) >> 1;
            if(copy.get(mid) < num) left = mid + 1;
            else right = mid;
        }
        int k = right+1; // right即查找的元素,这里+1是因为树状数组下标从1开始

        // 更新答案,查找以元素值(1,num-1)结尾的LIS的最大值
        res = Math.max(res, (int)tree.preMax(k) + 1);
        // 维护树状数组,更新以元素值(1,num)结尾的LIS的最大值
        tree.update(k+1, (int)tree.preMax(k) + 1);
    }
    return res;
}
}

// 树状数组模板(维护前缀最大值)
class BIT {
    private long[] tree;

    public BIT(int n) {
        tree = new long[n];
        Arrays.fill(tree, Long.MIN_VALUE);
    }

    public void update(int i, long val) {
        while (i < tree.length) {
            tree[i] = Math.max(tree[i], val);
            i += i & -i;
        }
    }

    public long preMax(int i) {
        long res = Long.MIN_VALUE;
        while (i > 0) {
            res = Math.max(res, tree[i]);
            i &= i - 1;
        }
        return res;
    }
}


---

## [673. 最长递增子序列的个数](https://leetcode.cn/problems/number-of-longest-increasing-subsequence/)

难度中等706

给定一个未排序的整数数组 `nums` , *返回最长递增子序列的个数***注意** 这个数列必须是 **严格** 递增的。

 

**示例 1:**

输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。


**示例 2:**

输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。


 

**提示:** 



- `1 <= nums.length <= 2000`
- `-106 <= nums[i] <= 106`

```java
class Solution {
    // 我们只需要在朴素 LIS 问题的基础上通过「记录额外信息」来进行求解即可。
    public int findNumberOfLIS(int[] nums) {
        int n = nums.length;
    // 定义f[i]为以nums[i]为结尾的最长上升子序列 长度
    // 定义g[i]为以nums[i]为结尾的最长上升子序列 个数
        int[] f = new int[n], g = new int[n];
        int max = 1; // 定义最大长度为max
        for(int i = 0; i < n; i++){
            f[i] = g[i] = 1;
            for(int j = 0; j < i; j++){
                if(nums[j] < nums[i]){ // nums[i] 可以接在 nums[j] 后面形成LIS

                    if(f[i] < f[j] + 1){
                        f[i] = f[j] + 1;
                        g[i] = g[j]; // 没有超过长度,g[i] 直接更新为g[j]
                    }else if(f[i] == f[j] + 1){
                        g[i] += g[j]; // f[i] + 1 等于 f[j] 长度时, 找到了符合新条件的前驱,进行累加
                    }
                }
            }
            max = Math.max(max, f[i]); // 更新长度
        }
        int ans = 0;
        for(int i = 0; i < n; i++){
            if(f[i] == max) ans += g[i];
        }
        return ans;
    }
}

1964. 找出到每个位置为止最长的有效障碍赛跑路线

难度困难32

你打算构建一些障碍赛跑路线。给你一个 下标从 0 开始 的整数数组 obstacles ,数组长度为 n ,其中 obstacles[i] 表示第 i 个障碍的高度。

对于每个介于 0n - 1 之间(包含 0n - 1)的下标 i ,在满足下述条件的前提下,请你找出 obstacles 能构成的最长障碍路线的长度:

  • 你可以选择下标介于 0i 之间(包含 0i)的任意个障碍。
  • 在这条路线中,必须包含第 i 个障碍。
  • 你必须按障碍在 obstacles 中的 出现顺序 布置这些障碍。
  • 除第一个障碍外,路线中每个障碍的高度都必须和前一个障碍 相同 或者 更高

返回长度为 n 的答案数组 ans ,其中 ans[i] 是上面所述的下标 i 对应的最长障碍赛跑路线的长度。

示例 1:

输入:obstacles = [1,2,3,2]
输出:[1,2,3,3]
解释:每个位置的最长有效障碍路线是:
- i = 0: [1], [1] 长度为 1
- i = 1: [1,2], [1,2] 长度为 2
- i = 2: [1,2,3], [1,2,3] 长度为 3
- i = 3: [1,2,3,2], [1,2,2] 长度为 3

示例 2:

输入:obstacles = [2,2,1]
输出:[1,2,1]
解释:每个位置的最长有效障碍路线是:
- i = 0: [2], [2] 长度为 1
- i = 1: [2,2], [2,2] 长度为 2
- i = 2: [2,2,1], [1] 长度为 1

示例 3:

输入:obstacles = [3,1,5,6,4,2]
输出:[1,1,2,3,2,2]
解释:每个位置的最长有效障碍路线是:
- i = 0: [3], [3] 长度为 1
- i = 1: [3,1], [1] 长度为 1
- i = 2: [3,1,5], [3,5] 长度为 2, [1,5] 也是有效的障碍赛跑路线
- i = 3: [3,1,5,6], [3,5,6] 长度为 3, [1,5,6] 也是有效的障碍赛跑路线
- i = 4: [3,1,5,6,4], [3,4] 长度为 2, [1,4] 也是有效的障碍赛跑路线
- i = 5: [3,1,5,6,4,2], [1,2] 长度为 2

提示:

  • n == obstacles.length
  • 1 <= n <= 105
  • 1 <= obstacles[i] <= 107

LIS贪心+二分变形题

class Solution {
    public int[] longestObstacleCourseAtEachPosition(int[] obstacles) {
        // LIS 贪心 + 二分的做法
        int n = obstacles.length;
        int[] res = new int[n];
        int[] g = new int[n]; //g[i] : 定义长度为 i+1 LIS 的末尾元素的最小值
        int len = 0;
        for(int k = 0; k < n; k++){
            int i = 0, j = len, num = obstacles[k];
        	while(i < j){ // 二分找到第一个大于num的位置,所以判断条件if(g[m] <= num)
                 int m = (i+j) / 2;
                if(g[m] <= num) i = m + 1;
                else j = m;
            }
            res[k] = j+1;
            g[i] = num;
            if(len == j) len++;
        }
        return res;
    }
}

1671. 得到山形数组的最少删除次数

难度困难31

我们定义 arr山形数组 当且仅当它满足:

  • arr.length >= 3

  • 存在某个下标i (从 0 开始) 满足

    0 < i < arr.length - 1
    

    且:

    • arr[0] < arr[1] < ... < arr[i - 1] < arr[i]
  • arr[i] > arr[i + 1] > ... > arr[arr.length - 1]

给你整数数组 nums ,请你返回将 nums 变成 山形状数组最少 删除次数。

示例 1:

输入:nums = [1,3,1]
输出:0
解释:数组本身就是山形数组,所以我们不需要删除任何元素。

示例 2:

输入:nums = [2,1,1,5,6,2,3,1]
输出:3
解释:一种方法是将下标为 0,1 和 5 的元素删除,剩余元素为 [1,5,6,3,1] ,是山形数组。

提示:

  • 3 <= nums.length <= 1000
  • 1 <= nums[i] <= 109
  • 题目保证 nums 删除一些元素后一定能得到山形数组。
class Solution {
    // 问题转化为 左右上升子序列的最长长度
    public int minimumMountainRemovals(int[] nums) {
        int n = nums.length;
        // 前面的最大长度
        int[] pre = new int[n];
        for(int i = 0; i < n; i++){
            for(int j = 0; j < i; j++){
                if(nums[j] < nums[i]){
                    pre[i] = Math.max(pre[i], pre[j]+1);
                }
            }
        }
        // 后面的最大长度
        int[] post = new int[n];
        for(int i = n-1; i >= 0; i--){
            for(int j = n-1; j > i; j--){
                if(nums[j] < nums[i]){
                    post[i] = Math.max(post[i], post[j]+1);
                }
            }
        }
        int res = n;
        for(int i = 1; i < n; i++){
            if(pre[i] != 0 && post[i] != 0 && n-pre[i]-post[i]-1 < res){
                res = n- pre[i] - post[i] - 1;
            }
        }
        return res;
    }
}

1626. 无矛盾的最佳球队

难度中等81收藏分享切换为英文接收动态反馈

假设你是球队的经理。对于即将到来的锦标赛,你想组合一支总体得分最高的球队。球队的得分是球队中所有球员的分数 总和

然而,球队中的矛盾会限制球员的发挥,所以必须选出一支 没有矛盾 的球队。如果一名年龄较小球员的分数 严格大于 一名年龄较大的球员,则存在矛盾。同龄球员之间不会发生矛盾。

给你两个列表 scoresages,其中每组 scores[i]ages[i] 表示第 i 名球员的分数和年龄。请你返回 所有可能的无矛盾球队中得分最高那支的分数

示例 1:

输入:scores = [1,3,5,10,15], ages = [1,2,3,4,5]
输出:34
解释:你可以选中所有球员。

示例 2:

输入:scores = [4,5,6,5], ages = [2,1,2,1]
输出:16
解释:最佳的选择是后 3 名球员。注意,你可以选中多个同龄球员。

示例 3:

输入:scores = [1,2,3,5], ages = [8,9,10,1]
输出:6
解释:最佳的选择是前 3 名球员。

提示:

  • 1 <= scores.length, ages.length <= 1000
  • scores.length == ages.length
  • 1 <= scores[i] <= 106
  • 1 <= ages[i] <= 1000

求和最大的递增子序列问题

class Solution {
    public int bestTeamScore(int[] scores, int[] ages) {
        int n =scores.length;
        int[][] zip = new int[n][2];
        for(int i = 0; i < n; i++){
            zip[i][0] = scores[i];
            zip[i][1] = ages[i];
        }
        // 按照年龄排序
        Arrays.sort(zip, (a, b) -> a[1] == b[1] ? a[0] - b[0] : a[1] - b[1]);
        // 找得分单调递增最大的序列和 (求和最大的递增子序列, 允许有相同元素)
        int[] dp = new int[n+1];
        for(int i = 0; i < n; i++){
            dp[i] = zip[i][0]; // 初始化,每个i位置上最大和是自己
        }
        int res = dp[0];
        for(int i = 0; i < n; i++){
            for(int j = 0; j < i; j++){
                //将和作为衡量标准,而不是递增子序列的长度。
                if(zip[i][0] >= zip[j][0] && dp[i] < dp[j] + zip[i][0]){
                    dp[i] = dp[j] + zip[i][0];
                }
                res = Math.max(res, dp[i]);
            }
        }
        return res;
    }
}

四、其他练习

337. 打家劫舍 III(树形DP,树上最大独立集)

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

示例 1:

在这里插入图片描述

输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
示例 2:

在这里插入图片描述

输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9

提示:

树的节点数在 [1, 104] 范围内
0 <= Node.val <= 104

题解:

三种方法解决树形动态规划问题-从入门级代码到高效

作者:reals
链接:https://leetcode.cn/problems/house-robber-iii/solution/san-chong-fang-fa-jie-jue-shu-xing-dong-tai-gui-hu/

说明:本题目本身就是动态规划的树形版本,通过此题解,可以了解一下树形问题在动态规划问题解法

我们通过三个方法不断递进解决问题

  • 解法一通过递归实现,虽然解决了问题,但是复杂度太高

  • 解法二通过解决方法一中的重复子问题,实现了性能的百倍提升

  • 解法三直接省去了重复子问题,性能又提升了一步

解法一、暴力递归 - 最优子结构(超时)

在解法一和解法二中,我们使用爷爷、两个孩子、4 个孙子来说明问题

首先来定义这个问题的状态

爷爷节点获取到最大的偷取的钱数呢

首先要明确相邻的节点不能偷,也就是爷爷选择偷,儿子就不能偷了,但是孙子可以偷

二叉树只有左右两个孩子,一个爷爷最多 2 个儿子,4 个孙子

根据以上条件,我们可以得出单个节点的钱该怎么算

4 个孙子偷的钱 + 爷爷的钱 VS 两个儿子偷的钱 哪个组合钱多,就当做当前节点能偷的最大钱数。这就是动态规划里面的最优子结构

由于是二叉树,这里可以选择计算所有子节点

4 个孙子投的钱加上爷爷的钱如下:int method1 = root.val + rob(root.left.left) + rob(root.left.right) + rob(root.right.left) + rob(root.right.right)
两个儿子偷的钱如下:int method2 = rob(root.left) + rob(root.right);

挑选一个钱数多的方案则int result = Math.max(method1, method2);

将上述方案写成代码如下

public int rob(TreeNode root) {
    if (root == null) return 0;

    int money = root.val;
    if (root.left != null) {
        money += (rob(root.left.left) + rob(root.left.right));
    }

    if (root.right != null) {
        money += (rob(root.right.left) + rob(root.right.right));
    }

    return Math.max(money, rob(root.left) + rob(root.right));
}

解法二、记忆化 - 解决重复子问题

针对解法一种速度太慢的问题,经过分析其实现,我们发现爷爷在计算自己能偷多少钱的时候,同时计算了 4 个孙子能偷多少钱,也计算了 2 个儿子能偷多少钱。这样在儿子当爷爷时,就会产生重复计算一遍孙子节点

于是乎我们发现了一个动态规划的关键优化点:重复子问题

我们这一步针对重复子问题进行优化,我们在做斐波那契数列时,使用的优化方案是记忆化,但是之前的问题都是使用数组解决的,把每次计算的结果都存起来,下次如果再来计算,就从缓存中取,不再计算了,这样就保证每个数字只计算一次。

由于二叉树不适合拿数组当缓存,我们这次使用哈希表来存储结果,TreeNode 当做 key,能偷的钱当做 value

class Solution {
    public int rob(TreeNode root) {
    HashMap<TreeNode, Integer> memo = new HashMap<>();
    return robInternal(root, memo);
	}

 	public int robInternal(TreeNode root, HashMap<TreeNode, Integer> memo) {
 	    if (root == null) return 0;
 	    if (memo.containsKey(root)) return memo.get(root);
 	    int money = root.val;
 	
 	    if (root.left != null) {
 	        money += (robInternal(root.left.left, memo) + robInternal(root.left.right, memo));
 	    }
 	    if (root.right != null) {
 	        money += (robInternal(root.right.left, memo) + robInternal(root.right.right, memo));
 	    }
 	    int result = Math.max(money, robInternal(root.left, memo) + robInternal(root.right, memo));
 	    memo.put(root, result);
 	    return result;
 	}
}

解法三:最优解法

上面两种解法用到了孙子节点,计算爷爷节点能偷的钱还要同时去计算孙子节点投的钱,虽然有了记忆化,但是还是有性能损耗。

我们换一种办法来定义此问题

每个节点可选择偷或者不偷两种状态,根据题目意思,相连节点不能一起偷

  • 当前节点选择偷时,那么两个孩子节点就不能选择偷了

  • 当前节点选择不偷时,两个孩子节点只需要拿最多的钱出来就行(两个孩子节点偷不偷没关系)

我们使用一个大小为 2 的数组来表示 int[] res = new int[2] 0 代表不偷,1 代表偷

任何一个节点能偷到的最大钱的状态可以定义为

  • **当前节点选择不偷:**当前节点能偷到的最大钱数 = 左孩子能偷到的钱 + 右孩子能偷到的钱

  • 当前节点选择偷:当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数

表示为公式如下

root[0] = Math.max(rob(root.left)[0], rob(root.left)[1]) + Math.max(rob(root.right)[0], rob(root.right)[1])
root[1] = rob(root.left)[0] + rob(root.right)[0] + root.val;

将公式做个变换就是代码啦

class Solution {
    public int rob(TreeNode root) {
        int[] result = robInternal(root);
        return Math.max(result[0], result[1]);
    }
    // result[] : 0代表不偷, 1代表偷
    public int[] robInternal(TreeNode root){
        if(root == null) return new int[2];
        int[] result = new int[2];
        int[] left = robInternal(root.left);
        int[] right = robInternal(root.right);
        // 当前节点不偷,最大钱数 : 左孩子能偷到的钱 + 右孩子能偷到的钱
        result[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        // 当前节点偷,最大钱数:左孩子不偷 + 右孩子不偷 + 节点值
        result[1] = left[0] + right[0] + root.val;
        return result;
    }
}

LIS练习题

1027. 最长等差数列

难度中等237

给你一个整数数组 nums,返回 nums 中最长等差子序列的长度

回想一下,nums 的子序列是一个列表 nums[i1], nums[i2], ..., nums[ik] ,且 0 <= i1 < i2 < ... < ik <= nums.length - 1。并且如果 seq[i+1] - seq[i]( 0 <= i < seq.length - 1) 的值都相同,那么序列 seq 是等差的。

示例 1:

输入:nums = [3,6,9,12]
输出:4
解释: 
整个数组是公差为 3 的等差数列。

示例 2:

输入:nums = [9,4,7,2,10]
输出:3
解释:
最长的等差子序列是 [4,7,10]。

示例 3:

输入:nums = [20,1,15,3,10,5,8]
输出:4
解释:
最长的等差子序列是 [20,15,10,5]。

提示:

  • 2 <= nums.length <= 1000
  • 0 <= nums[i] <= 500

题解:https://leetcode.cn/problems/longest-arithmetic-subsequence/solution/zui-chang-deng-chai-shu-lie-by-zai-jian-u21ci/

状态定义:dp[i][d]: 表示以数组下标 i 处的元素结尾、公差为 d 的等差数列的最大长度。

等差数列至少包含 2 个数,也就是说 1 个数不能构成等差数列,任意 2 个元素都能构成长度为 2 的等差数列。

假设现在有一个子序列元素 x , y,它是一个等差数列, 公差为 d,考虑 z 能否加入到 y 后面?

如果 z 能加入,意味着 z-y=y-x, 还可以是 z-d=y

我们是从小到大推导 dp 的,我们在计算 dp[k][] 时,dp[0…k-1][] 已经计算过了,那么 dp[k][] 能否从子问题推导过来呐? 可以的。

  • dp[i][d] = max(dp[i][d], dp[j][d] + 1), 0 <= j < i
class Solution {
    /**
        dp[i][d]:表示第i个数,与前面的数以差为d时,能构成的最长等差数列长度。
        dp[i][d] = max(dp[i][d], dp[j][d] + 1), 0 <= j < i
     */
    public int longestArithSeqLength(int[] nums) {
        int n = nums.length;
        int ans = 0;
        int[][] dp = new int[n][1010];
        for(int i = 0; i < n; i++) Arrays.fill(dp[i], 1);
        for(int i = 0; i < n; i++){
            for(int j = i-1; j >= 0; j--){
                // 表示 nums[i] 与 nums[j] 以差为 d 构成等差数列
                int d = nums[i] - nums[j] + 500;// 统一加偏移量,使下标非负
                // dp[i][d]表示:nums[i]以差为d能与前面的数构成的等差数列的长度
                dp[i][d] = Math.max(dp[i][d], dp[j][d] + 1);
                ans = Math.max(ans, dp[i][d]);
            }
        }
        return ans;
    }
}

将初始化后移:

状态转移方程有了,现在我们考虑 basecase,也就是初始化的问题,我们需要两层 for 循环给所有 dp[i][j]初始化为 1

  • 初始化完了就可进行计算再返回结果,另外比较特殊的是,由于是统一初始化成相同的值,“地位平等”,使得也可以不用先初始化,在没有显式的初始化的基础上,算完之后,再将结果 +1,也能得到相同的结果,并且后者效率高于前者(后者相较于前者少了 2 层 for 循环的时间)
class Solution {
    /**
        dp[i][d]:表示第i个数,与前面的数以差为d时,能构成的最长等差数列长度。
        dp[i][d] = max(dp[i][d], dp[j][d] + 1), 0 <= j < i
     */
    public int longestArithSeqLength(int[] nums) {
        int n = nums.length;
        int ans = 0;
        int[][] dp = new int[n][1010];
        for(int i = 0; i < n; i++){
            for(int j = i-1; j >= 0; j--){
                // 表示 nums[i] 与 nums[j] 以差为 d 构成等差数列
                int d = nums[i] - nums[j] + 500;
                // dp[i][d]表示:nums[i]以差为d能与前面的数构成的等差数列的长度
                dp[i][d] = Math.max(dp[i][d], dp[j][d] + 1);
                ans = Math.max(ans, dp[i][d]);
            }
        }
        return ans + 1;
    }
}

LCS练习题

1187. 使数组严格递增

难度困难107

给你两个整数数组 arr1arr2,返回使 arr1 严格递增所需要的最小「操作」数(可能为 0)。

每一步「操作」中,你可以分别从 arr1arr2 中各选出一个索引,分别为 ij0 <= i < arr1.length0 <= j < arr2.length,然后进行赋值运算 arr1[i] = arr2[j]

如果无法让 arr1 严格递增,请返回 -1

示例 1:

输入:arr1 = [1,5,3,6,7], arr2 = [1,3,2,4]
输出:1
解释:用 2 来替换 5,之后 arr1 = [1, 2, 3, 6, 7]。

示例 2:

输入:arr1 = [1,5,3,6,7], arr2 = [4,3,1]
输出:2
解释:用 3 来替换 5,然后用 4 来替换 3,得到 arr1 = [1, 3, 4, 6, 7]。

示例 3:

输入:arr1 = [1,5,3,6,7], arr2 = [1,6,3,3]
输出:-1
解释:无法使 arr1 严格递增。

提示:

  • 1 <= arr1.length, arr2.length <= 2000
  • 0 <= arr1[i], arr2[i] <= 10^9

题解:https://leetcode.cn/problems/make-array-strictly-increasing/solution/zui-chang-di-zeng-zi-xu-lie-de-bian-xing-jhgg/

为方便描述,下文将 arr1 简记为 a ,arr2 简记为 b。问题等价于从 a 中找到一个最长严格递增子序列 lis,使得把不在 lis 中的元素替换成b中的元素后,a 是严格递增的,求不在 lis 中的元素个数的最小值

对于最长递增子序列问题 (或者一般的动态规划问题),通常都可以用[选或不选]和[枚举选哪个]来启发思考。

方法一:选还是不选

例如 a=[0,4,2,2],b=[1,2,3,4],假如 a[3] =2 换成了b[3] = 4,那么对于 a[2] =2 来说

  • 选择不替换,问题变成[把[0,4] 替换成严格递增数组,且数组的最后一个数小于 2,所需要的最小操作数]。

  • 选择替换,那么应该替换得越大越好,但必须小于 4 (因为a[3] 替换成了4),那么换成3最佳,问题变成[把[0,4]替换成严格递增数组,且最后一个数小于 3,所需要的最小操作数].

    把从 a[0] 到 a[1] 的这段前缀替换成严格递增数组,且数组的最后一个数小于 pre,所需要的最小操作数。记为 dfs(i,pre)

记忆化搜索

class Solution {
    int[] a, b;
    Map<Integer, Integer> memo[];
    public int makeArrayIncreasing(int[] arr1, int[] arr2) {
        this.a = arr1;
        this.b = arr2;
        Arrays.sort(b);// 为了能二分查找,对b排序
        int n = a.length;
        memo = new HashMap[n];
        Arrays.setAll(memo,e -> new HashMap<>());
        // 假设 a[n-1] 右侧有个无穷大的数
        int ans = dfs(n-1, Integer.MAX_VALUE/2);
        return ans < Integer.MAX_VALUE/3 ? ans : -1;
    }

    /**
    例如 a=[0,4,2,2],b=[1,2,3,4],假如 a[3] =2 换成了b[3] = 4,那么对于 a[2] =2 来说
        选择不替换,问题变成[把[0,4] 替换成严格递增数组,且数组的最后一个数小于 2,所需要的最小操作数]。
        选择替换,那么应该替换得越大越好,但必须小于 4 (因为a[3] 替换成了4),那么换成3最佳,问题变成[把[0,4]替换成严格递增数组,且最后一个数小于 3,所需要的最小操作数]。
     */
    // 把从 a[0] 到 a[1] 的这段前缀替换成严格递增数组,且数组的最后一个数小于 pre,所需要的最小操作数。记为 dfs(i,pre)
    public int dfs(int i, int pre){
        if(i < 0) return 0;
        if(memo[i].containsKey(pre)) return memo[i].get(pre);
        // 不替换
        int res = a[i] < pre ? dfs(i-1, a[i]) : Integer.MAX_VALUE/2;
        // 替换
        // 二分查找b中小于pre的最大数的下标 
        int k = lowerbound(b, pre) - 1; 
        if(k >= 0) { // a[i]替换成小于 pre 的最大数
            res = Math.min(res, dfs(i-1, b[k]) + 1);
        }
        memo[i].put(pre, res);
        return res;
    }   
    // 返回大于等于key的第一个元素的下标
    public int lowerbound(int[] nums, int target){
        int left = 0, right = nums.length;
        while(left < right){
            int mid = (left + right) / 2;
            if(nums[mid] < target) left = mid + 1;
            else right = mid;
        }
        return left;
    }
}

方法二:枚举选哪个

在方法二中,我们把重点放在 s 上,关注哪些 a[i] 没有被替换,那么答案就是 n -length(lis)

记忆化搜索

仿照最长递增子序列的状态定义,用 dfs(i) 表示以 a[i] 结尾的 lis 的长度,这里a[i] 没有被替换

  • 枚举a[i] 左侧最近的没有被替换的元素a[j],那么必须满足从 a[j+1]a[i-1]的这段数组,能被替换成b中的元素,且替换后从a[j]a[i] 是严格递增的。为了保证替换的元素互不相同,需要对 b去重。

b[k]>=a[i] 的最小元素,注: 即使 b[k] 不存在也没关系,下面不会用到这个数

a[i -1] 最大可以替换成 b 中小于 a[i] 的最大元素,即 b[k - 1],然后a[i - 2] 最大可以替换成 b[k - 2],…,a[j +1] 最大可以替换成b[k-(i-j-1)]

注: 从a[j+1]a[i-1]一共有i-j-1个数。
所以,只有满足 b[k -(i -j- 1)] >a[j],才能完成替换操作。此时更新 dfs(i) = max(dfs(i), dfs(j) + 1)
注: 要求k-(i-j-1)>= 0,也就是j>=i-k -1

class Solution {
    int[] a, b, memo;
    int m;
    public int makeArrayIncreasing(int[] arr1, int[] arr2) {
        this.a = arr1;
        this.b = arr2;
        Arrays.sort(b);// 为了能二分查找,对b排序
        for(int i = 1; i < b.length; i++){
            if(b[m] != b[i]){
                b[++m] = b[i]; // 原地去重
            }
        }
        ++m;
        int n = a.length;
        memo = new int[n+1]; // 0 表示还没有计算过
        int ans = dfs(n);
        return ans < 0 ? -1 : n + 1 - ans;
    }

    public int dfs(int i){
        if(memo[i] != 0) return memo[i];
        int x = i < a.length ? a[i] : Integer.MAX_VALUE;
        int k = lowerbound(b, m, x);
        int res = k < i ? Integer.MIN_VALUE : 0; // 小于 a[i] 的数全部替换
        if(i > 0 && a[i-1] < x) { // 无替换
            res = Math.max(res, dfs(i-1));
        } 
        for(int j = i-2; j > i-k-1 && j >= 0; j--){
            if (b[k - (i - j - 1)] > a[j])
                // a[j+1] 到 a[i-1] 替换成 b[k-(i-j-1)] 到 b[k-1]
                res = Math.max(res, dfs(j));
        }
        return memo[i] = ++res; // 把 +1 移到这里,表示 a[i] 不变
    }

    
    // 返回大于等于key的第一个元素的下标
    public int lowerbound(int[] nums, int right, int target){
        int left = 0;
        while(left < right){
            int mid = (left + right) / 2;
            if(nums[mid] < target) left = mid + 1;
            else right = mid;
        }
        return left;
    }
}

翻译成递推

class Solution {
    public int makeArrayIncreasing(int[] a, int[] b) {
        int n = a.length, m = 0;
        Arrays.sort(b);// 为了能二分查找,对b排序
        for(int i = 1; i < b.length; i++){
            if(b[m] != b[i]){
                b[++m] = b[i]; // 原地去重
            }
        }
        ++m;
        var f = new int[n + 1];
        for (int i = 0; i <= n; i++) {
            int x = i < n ? a[i] : Integer.MAX_VALUE;
            int k = lowerbound(b, m, x);
            int res = k < i ? Integer.MIN_VALUE : 0; // 小于 a[i] 的数全部替换
            if (i > 0 && a[i - 1] < x) // 无替换
                res = Math.max(res, f[i - 1]);
            for (int j = i - 2; j > i - k - 1 && j >= 0; --j)
                if (b[k - (i - j - 1)] > a[j])
                    // a[j+1] 到 a[i-1] 替换成 b[k-(i-j-1)] 到 b[k-1]
                    res = Math.max(res, f[j]);
            f[i] = res + 1; // 把 +1 移到这里,表示 a[i] 不替换
        }
        return f[n] < 0 ? -1 : n + 1 - f[n];    
    }
  
    // 返回大于等于key的第一个元素的下标
    public int lowerbound(int[] nums, int right, int target){
        int left = 0;
        while(left < right){
            int mid = (left + right) / 2;
            if(nums[mid] < target) left = mid + 1;
            else right = mid;
        }
        return left;
    }
}

五、子数组DP问题

练习

53. 最大子数组和

难度中等6127

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例 2:

输入:nums = [1]
输出:1

示例 3:

输入:nums = [5,4,-1,7,8]
输出:23

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104

题解:

记忆化搜索(超时)

class Solution {
    int[] nums;
    int[] cache;
    public int maxSubArray(int[] nums) {
        this.nums = nums;
        int n = nums.length;
        cache = new int[n];
        Arrays.fill(cache, -1);
        int res = Integer.MIN_VALUE;
        for(int i = 0; i < n; i++)
            res = Math.max(res, dfs(i));
        return res;
    }   

    // 定义dfs(i)表示子数组右端点是arr[i]时的子数组最大和(表示以 nums[i] 结尾 的 连续 子数组的最大和。)
    // 转移:考虑第i位接不接第i-1位
    //      接:dfs(i) = dfs(i-1) + nums[i]
    //      不接:dfs(i) = nums[i]
    //  两者取最大值
    // 递归边界:dfs(-1) = -inf , 因为子数组最少包含一个元素
    // 递归入口:dfs(range(n)) 枚举以每一位为结尾的最大子数组和
    public int dfs(int i){
        if(i < 0) return Integer.MIN_VALUE / 2;
        if(cache[i] >= 0) return cache[i];
        return cache[i] = Math.max(dfs(i-1), 0) + nums[i];
    }
}

记忆化搜索转为递推

class Solution {
    public int maxSubArray(int[] nums) {
        int n = nums.length;
        int res = Integer.MIN_VALUE;
        int[] f = new int[n+1];
        f[0] = Integer.MIN_VALUE / 2; // 表示 i < 0 的情况
        for(int i = 0; i < n; i++){
            f[i+1] = Math.max(f[i], 0) + nums[i];
            res = Math.max(f[i+1], res);
        }
        return res;
    }
}

空间优化:考虑f[i+1]只与f[i]有关,所以只需要一个变量就可以表示

class Solution {
    public int maxSubArray(int[] nums) {
        int n = nums.length;
        int res = Integer.MIN_VALUE;
        int f = Integer.MIN_VALUE / 2;
        for(int i = 0; i < n; i++){
            f = Math.max(f, 0) + nums[i];
            res = Math.max(f, res);
        }
        return res;
    }
}

1186. 删除一次得到子数组最大和

难度中等181

给你一个整数数组,返回它的某个 非空 子数组(连续元素)在执行一次可选的删除操作后,所能得到的最大元素总和。换句话说,你可以从原数组中选出一个子数组,并可以决定要不要从中删除一个元素(只能删一次哦),(删除后)子数组中至少应当有一个元素,然后该子数组(剩下)的元素总和是所有子数组之中最大的。

注意,删除一个元素后,子数组 不能为空

示例 1:

输入:arr = [1,-2,0,3]
输出:4
解释:我们可以选出 [1, -2, 0, 3],然后删掉 -2,这样得到 [1, 0, 3],和最大。

示例 2:

输入:arr = [1,-2,-2,3]
输出:3
解释:我们直接选出 [3],这就是最大和。

示例 3:

输入:arr = [-1,-1,-1,-1]
输出:-1
解释:最后得到的子数组不能为空,所以我们不能选择 [-1] 并从中删去 -1 来得到 0。
     我们应该直接选择 [-1],或者选择 [-1, -1] 再从中删去一个 -1。

提示:

  • 1 <= arr.length <= 105
  • -104 <= arr[i] <= 104

记忆化搜索 ==> 动态规划

子数组问题dfs遍历右端点,是否删除多一个变量即可

最基础的想法是,对于每一个负数,枚举删除它的情况,然后就能得到最优解。但是这么做要么时间复杂度很高,因为最坏的情况是一个n*n维的dp矩阵。

仔细再想一下的话就可以得出:由于只能删除一个数,所以如果要删除位置i的数,其实只要跟删除i-1之前的最优情况对比就能得到最优解,因为从i+1开始对于所有前面删过数的情况,后面的最大子序列都是一样的。所以可以只用两个list就记录下来所有的最优情况,等于做了一个压缩。

因此,对于位置i,如果前面删除了位置j,最优解其实是 {前面有删除的情况+arr[i], arr[i], 删除位置i} 这三者中的最大值,也就是max(dp1[i-1] + arr[i], arr[i], dp[i])。其中dp1是记录有删除1个数据情况下的最优解,dp则是没有删除数据的最优解,删除位置i等价于不删除情况下dp[i-1]的值。

想明白之后代码就很简单了,几行就写好了

https://leetcode.cn/problems/maximum-subarray-sum-with-one-deletion/solution/jiao-ni-yi-bu-bu-si-kao-dong-tai-gui-hua-hzz6/

class Solution {
    int[] arr;
    int[][] cache;
    public int maximumSum(int[] arr) {
        int n = arr.length;
        int res = Integer.MIN_VALUE;
        this.arr = arr;
        cache = new int[n][2];
        for(int i = 0; i < n; i++)
            Arrays.fill(cache[i], -1);
        for(int i = 0; i < n; i++)
            res = Math.max(res, Math.max(dfs(i, 0), dfs(i, 1)));
        return res;
    }

    // 定义dfs(i, j)表示子数组右端点是arr[i], 不能/必须(j = 0,1)删除数字的情况下,子数组元素和的最大值
    // 转移: 
    //   如果j = 0 不能删除的情况下:
    //      如果不选arr[i]左边的数:dfs(i, 0) = arr[i]
    //      如果选arr[i]左边的数,dfs(i, 0) = dfs(i-1, 0) + arr[i]
    //   如果j = 1 必须删除的情况下:
    //      如果不删除arr[i]: dfs(i, 1) = dfs(i-1, 1) + arr[i]
    //      如果删除arr[i]:dfs(i, 1) = dfs(i-1, 0)
    //  这几种情况取最大值
    //      dfs(i, 0) = max(dfs(i-1, 0), 0)+ arr[i]
    //      dfs(i, 1) = max(dfs(i-1, 0), dfs(i-1, 1) + arr[i])
    // 递归边界:dfs( < 0, j) = -inf, 题目要求子数组不能为空,所以这种情况不合法
    // 递归入口:dfs(i, j),枚举右端点i
    public int dfs(int i, int j){
        if(i < 0) return Integer.MIN_VALUE / 2;
        if(cache[i][j] >= 0) return cache[i][j];
        int res = 0;
        if(j == 0)
            res = Math.max(dfs(i-1, 0), 0) + arr[i];
        else
            res = Math.max(dfs(i-1, 1) + arr[i], dfs(i-1, 0));
        return cache[i][j] = res;
    }
}

记忆化搜索转递推

class Solution {
    public int maximumSum(int[] arr) {
        int n = arr.length;
        int res = Integer.MIN_VALUE;
        int[][] f = new int[n+1][2];
        // 子数组为空的情况,题目要求子数组不能为空
        for(int i = 0; i < n+1; i++)
            Arrays.fill(f[i], Integer.MIN_VALUE / 2);
        for(int i = 0; i < n; i++){
            f[i+1][0] = Math.max(f[i][0], 0) + arr[i];
            f[i+1][1] = Math.max(f[i][1] + arr[i], f[i][0]);
            res = Math.max(res, Math.max(f[i+1][0], f[i+1][1]));
        }
        // 答案为所有f[i][j] 的最大值
        return res;
    }
}

空间优化

在计算f[i+1]时只会用到f[i],因此只需要两个状态表示j = 0, 1

  • 注意计算顺序,必须先计算f[i],再计算f[0]
class Solution {
    public int maximumSum(int[] arr) {
        int n = arr.length;
        int res = Integer.MIN_VALUE;
        int[] f = new int[2];
        // 子数组为空的情况,题目要求子数组不能为空
        Arrays.fill(f, Integer.MIN_VALUE / 2);
        for(int i = 0; i < n; i++){
            f[1] = Math.max(f[1] + arr[i], f[0]);
            f[0] = Math.max(f[0], 0) + arr[i];
            res = Math.max(res, Math.max(f[0], f[1]));
        }
        // 答案为所有f[i][j] 的最大值
        return res;
    }
}

2606. 找到最大开销的子字符串

难度中等6

给你一个字符串 s ,一个字符 互不相同 的字符串 chars 和一个长度与 chars 相同的整数数组 vals

子字符串的开销 是一个子字符串中所有字符对应价值之和。空字符串的开销是 0

字符的价值 定义如下:

  • 如果字符不在字符串 chars 中,那么它的价值是它在字母表中的位置(下标从 1 开始)。
    • 比方说,'a' 的价值为 1'b' 的价值为 2 ,以此类推,'z' 的价值为 26
  • 否则,如果这个字符在 chars 中的位置为 i ,那么它的价值就是 vals[i]

请你返回字符串 s 的所有子字符串中的最大开销。

示例 1:

输入:s = "adaa", chars = "d", vals = [-1000]
输出:2
解释:字符 "a" 和 "d" 的价值分别为 1 和 -1000 。
最大开销子字符串是 "aa" ,它的开销为 1 + 1 = 2 。
2 是最大开销。

示例 2:

输入:s = "abc", chars = "abc", vals = [-1,-1,-1]
输出:0
解释:字符 "a" ,"b" 和 "c" 的价值分别为 -1 ,-1 和 -1 。
最大开销子字符串是 "" ,它的开销为 0 。
0 是最大开销。

提示:

  • 1 <= s.length <= 105
  • s 只包含小写英文字母。
  • 1 <= chars.length <= 26
  • chars 只包含小写英文字母,且 互不相同
  • vals.length == chars.length
  • -1000 <= vals[i] <= 1000

题解:

记忆化搜索

class Solution {
    Map<Character, Integer> costs;
    String s;
    int[] cache;
    public int maximumCostSubstring(String s, String chars, int[] vals) {
        this.s = s;
        costs = new HashMap<>();
        for(int i = 0; i < vals.length; i++){
            costs.put(chars.charAt(i), vals[i]);
        }
        for(int i = 0; i < 26; i++){
            char c = (char)(i + 'a');
            if(costs.containsKey(c)) continue;
            costs.put(c, i + 1);
        }
        cache = new int[s.length()];
        Arrays.fill(cache, -1);
        int res = 0;
        for(int i = 0; i < s.length(); i++)
            res = Math.max(res, dfs(i));
        return res;
    }

    // 定义dfs(i)表示以s[i]结尾的子字符串的最大开销
    // 转移:考虑第i位接不接第i-1位
    //      接:dfs(i) = dfs(i-1) + cost(i)
    //      不接:dfs(i) = cost(i)
    //  两者取最大值
    // 递归边界:dfs(-1) = 0 , 因为空串的价值为0
    // 递归入口:dfs(range(n)) 枚举以每一位为结尾的最大子数组和
    public int dfs(int i){
        if(i < 0) return 0;
        if(cache[i] >= 0) return cache[i];
        return cache[i] = Math.max(dfs(i-1), 0) + costs.get(s.charAt(i));
    }
}	

记忆化搜索转递推

class Solution {
    Map<Character, Integer> costs;
    public int maximumCostSubstring(String s, String chars, int[] vals) {
        costs = new HashMap<>();
        for(int i = 0; i < vals.length; i++){
            costs.put(chars.charAt(i), vals[i]);
        }
        for(int i = 0; i < 26; i++){
            char c = (char)(i + 'a');
            if(costs.containsKey(c)) continue;
            costs.put(c, i + 1);
        }
        int[] f = new int[s.length() + 1];
        int res = 0;
        for(int i = 0; i < s.length(); i++){
            f[i+1] = Math.max(f[i], 0) + costs.get(s.charAt(i));
            res = Math.max(res, f[i+1]);
        }
        return res;
    }

}

空间优化

class Solution {
    Map<Character, Integer> costs;
    public int maximumCostSubstring(String s, String chars, int[] vals) {
        costs = new HashMap<>();
        for(int i = 0; i < vals.length; i++){
            costs.put(chars.charAt(i), vals[i]);
        }
        for(int i = 0; i < 26; i++){
            char c = (char)(i + 'a');
            if(costs.containsKey(c)) continue;
            costs.put(c, i + 1);
        }
        int f = 0;
        int res = 0;
        for(int i = 0; i < s.length(); i++){
            f = Math.max(f, 0) + costs.get(s.charAt(i));
            res = Math.max(res, f);
        }
        return res;
    }

}

918. 环形子数组的最大和

难度中等502

给定一个长度为 n环形整数数组 nums ,返回 nums 的非空 子数组 的最大可能和

环形数组 意味着数组的末端将会与开头相连呈环状。形式上, nums[i] 的下一个元素是 nums[(i + 1) % n]nums[i] 的前一个元素是 nums[(i - 1 + n) % n]

子数组 最多只能包含固定缓冲区 nums 中的每个元素一次。形式上,对于子数组 nums[i], nums[i + 1], ..., nums[j] ,不存在 i <= k1, k2 <= j 其中 k1 % n == k2 % n

示例 1:

输入:nums = [1,-2,3,-2]
输出:3
解释:从子数组 [3] 得到最大和 3

示例 2:

输入:nums = [5,-3,5]
输出:10
解释:从子数组 [5,5] 得到最大和 5 + 5 = 10

示例 3:

输入:nums = [3,-2,2,-3]
输出:3
解释:从子数组 [3] 和 [3,-2,2] 都可以得到最大和 3

提示:

  • n == nums.length
  • 1 <= n <= 3 * 104
  • -3 * 104 <= nums[i] <= 3 * 104

题解:如果子数组没有跨越边界,只有中间一段,就是普通的最大子数组和。 如果子数组跨越边界,那么正难则反,考虑不在子数组中的数,它们组成了一个中间的子数组,相当于求最小子数组和。然后用整个数组的元素和,减去中间的最小子数组和,就得到了跨越边界的最大子数组和了。

根据题解可知: 环形子数组的最大和具有两种可能,一种是不使用环的情况,另一种是使用环的情况

1、不使用环的情况时,直接通过53题的思路,逐步求出整个数组中的最大子序和即可

2、使用到了环,则必定包含 A[n-1]和 A[0]两个元素且说明从A[1]到A[n-2]这个子数组中必定包含负数。【否则只通过一趟最大子序和就可以的出结果】

因此只需要把A[1]-A[n-2]间这些负数的最小和求出来

用整个数组的和 sum减掉这个负数最小和即可实现原环型数组的最大和

class Solution {
    public int maxSubarraySumCircular(int[] nums) {
        int n = nums.length;
        int maxsum = nums[0]; // 最大子数组和
        int cur = nums[0];
        for(int i = 1; i < n; i++){
            cur = Math.max(cur, 0) + nums[i];
            maxsum = Math.max(maxsum, cur);
        }
        if(n > 2){ // 只有当至少有三个元素时才会出现第二种情况
            int sum = nums[0] + nums[n-1]; // 环形的肯定包含首尾两个元素
            int minsum = nums[1]; 
            cur = 0;
            // 求 [1, n-2] 的最小子数组和minsum
            for(int i = 1; i < n-1; i++){
                sum += nums[i];
                cur = Math.min(cur, 0) + nums[i];
                minsum = Math.min(minsum, cur);
            }
            return Math.max(maxsum, sum - minsum);
        }
        return maxsum;
    }
}

2321. 拼接数组的最大分数

难度困难30

给你两个下标从 0 开始的整数数组 nums1nums2 ,长度都是 n

你可以选择两个整数 leftright ,其中 0 <= left <= right < n ,接着 交换 两个子数组 nums1[left...right]nums2[left...right]

  • 例如,设 nums1 = [1,2,3,4,5]nums2 = [11,12,13,14,15] ,整数选择 left = 1right = 2,那么 nums1 会变为 [1,***12\*,\*13\***,4,5]nums2 会变为 [11,***2,3***,14,15]

你可以选择执行上述操作 一次 或不执行任何操作。

数组的 分数sum(nums1)sum(nums2) 中的最大值,其中 sum(arr) 是数组 arr 中所有元素之和。

返回 可能的最大分数

子数组 是数组中连续的一个元素序列。arr[left...right] 表示子数组包含 nums 中下标 leftright 之间的元素**(含** 下标 leftright 对应元素**)**。

示例 1:

输入:nums1 = [60,60,60], nums2 = [10,90,10]
输出:210
解释:选择 left = 1 和 right = 1 ,得到 nums1 = [60,90,60] 和 nums2 = [10,60,10] 。
分数为 max(sum(nums1), sum(nums2)) = max(210, 80) = 210 。

示例 2:

输入:nums1 = [20,40,20,70,30], nums2 = [50,20,50,40,20]
输出:220
解释:选择 left = 3 和 right = 4 ,得到 nums1 = [20,40,20,40,20] 和 nums2 = [50,20,50,70,30] 。
分数为 max(sum(nums1), sum(nums2)) = max(140, 220) = 220 。

示例 3:

输入:nums1 = [7,11,13], nums2 = [1,1,1]
输出:31
解释:选择不交换任何子数组。
分数为 max(sum(nums1), sum(nums2)) = max(31, 3) = 31 。

提示:

  • n == nums1.length == nums2.length
  • 1 <= n <= 105
  • 1 <= nums1[i], nums2[i] <= 104

先计算nums1-nums2和nums2-nums1的差数组diff1和diff2。然后我们就可以在diff数组上求连续子数组的最大和,最大和对应的就是最佳替换区间所带来的收益。

class Solution {
    // 设s1 = sum(nums1)
    // 交换[left, right]范围后,对于s1',有
    // s1' = s1 - (nums1[left]+...+nums1[right]) + (nums2[left]+ ...+nums2[right])
    // 合并相同下标,等式右侧变形位
    // s1' = s1 + (nums2[left] - nums1[left]) + ... + (nums2[right] - nums1[right])
    // 设diff[i] = nums2[i] - nums1[i],等式变为
    // s1' = s1 + diff[left] + diff[right]
    // 为了最大化上式,我们需要最大化diff数组的和 ==> 53.最大子数组和(允许子数组为空)
    // 对于nums[2]同理
    // 求这两者的最大值
    public int maximumsSplicedArray(int[] nums1, int[] nums2) {
        int n = nums1.length, sum1 = 0, sum2 = 0;
        int[] diff1 = new int[n], diff2 = new int[n];
        for(int i = 0; i < n; i++){
            sum1 += nums1[i];
            sum2 += nums2[i];
            diff1[i] = nums2[i] - nums1[i];
            diff2[i] = nums1[i] - nums2[i];
        }
        // 先求 nums1 的最大值
        int mx1 = 0, f = 0;
        for(int i = 0; i < n; i++){
            f = Math.max(f, 0) + diff1[i];
            mx1 = Math.max(mx1, f); 
        }
        // 求 nums2 的最大值
        int mx2 = 0, f2 = 0;
        for(int i = 0; i < n; i++){
            f2 = Math.max(f2, 0) + diff2[i];
            mx2 = Math.max(mx2, f2);
        }
        return Math.max(sum1 + mx1, sum2 + mx2);
    }
}

1749. 任意子数组和的绝对值的最大值

难度中等45

给你一个整数数组 nums 。一个子数组 [numsl, numsl+1, ..., numsr-1, numsr]和的绝对值abs(numsl + numsl+1 + ... + numsr-1 + numsr)

请你找出 nums和的绝对值 最大的任意子数组(可能为空),并返回该 最大值

abs(x) 定义如下:

  • 如果 x 是负整数,那么 abs(x) = -x
  • 如果 x 是非负整数,那么 abs(x) = x

示例 1:

输入:nums = [1,-3,2,3,-4]
输出:5
解释:子数组 [2,3] 和的绝对值最大,为 abs(2+3) = abs(5) = 5 。

示例 2:

输入:nums = [2,-5,1,-4,3,-2]
输出:8
解释:子数组 [-5,1,-4] 和的绝对值最大,为 abs(-5+1-4) = abs(-8) = 8 。

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104

方法一: 动态规划

class Solution {
    public int maxAbsoluteSum(int[] nums) {
        // 1. 找子数组最大和 2. 找子数组最小和 取abs最大值
        int mx1 = 0, cur = 0;
        for(int x : nums){
            cur = Math.max(cur + x, x);
            mx1 = Math.max(mx1, cur);
        }
        int mn2 = 0, cur2 = 0;
        for(int x : nums){
            cur2 = Math.min(cur2 + x, x);
            mn2 = Math.min(mn2, cur2);
        }
        return Math.max(Math.abs(mn2), mx1);
    }
}

方法二:前缀和

https://leetcode.cn/problems/maximum-absolute-sum-of-any-subarray/solution/liang-chong-fang-fa-dong-tai-gui-hua-qia-dczr/

由于子数组和等于两个前缀和的差,那么取前缀和中的最大值与最小值,它俩的差就是答案。

如果最大值在最小值右边,那么算的是最大子数组和。

如果最大值在最小值左边,那么算的是最小子数组和的绝对值(相反数)。

补充说明:对于 53. 最大子数组和 这题,可以枚举前缀和,同时维护前面的前缀和的最小值 minS,用当前前缀和减去 minS 就是以这个位置结尾的子数组的最大值了。

class Solution {
    public int maxAbsoluteSum(int[] nums) {
        int s = 0, mx = 0, mn = 0;
        for(int x : nums){
            s += x;
            if(s > mx) mx = s;
            else if(s < mn) mn = s;
        }
        return mx - mn;
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值