70.【必备】子数组最大累加和问题与扩展-下

本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~

网课链接:算法讲解071【必备】子数组最大累加和问题与扩展-下_哔哩哔哩_bilibili

一.乘积最大子数组

题目:乘积最大子数组

算法原理

  • 整体原理
    • 由于要找乘积最大的非空连续子数组,需要考虑到数组中可能存在负数的情况。因为一个负数乘以一个负数会变成正数,这可能会使原本较小的乘积变为较大的乘积。所以不能简单地用动态规划只记录最大的乘积,还需要记录最小的乘积。
  • 具体步骤
    • 初始化
      • 首先,将结果ans、最小值min和最大值max都初始化为数组的第一个元素nums[0]。这里假设数组至少有一个元素。
    • 遍历数组
      • 从数组的第二个元素(索引为1)开始遍历数组。
      • 对于每个元素nums[i],计算当前的最小值curmin和最大值curmax
        • curmin = Math.min(nums[i], Math.min(min * nums[i], max * nums[i])):这一步是计算当前可能的最小值。它考虑了三种情况,一是当前元素nums[i]本身,二是当前最小值min乘以nums[i](如果min为负数,nums[i]为负数,乘积可能是较大的正数),三是当前最大值max乘以nums[i](如果max为正数,nums[i]为负数,乘积会变小)。
        • curmax = Math.max(nums[i], Math.max(min * nums[i], max * nums[i])):这一步是计算当前可能的最大值。同样考虑了三种情况,一是当前元素nums[i]本身,二是当前最小值min乘以nums[i](如果min为负数,nums[i]为负数,乘积可能是较大的正数),三是当前最大值max乘以nums[i](如果max为正数,nums[i]为正数,乘积会更大)。
      • 更新最小值和最大值
        • min更新为curminmax更新为curmax
      • 更新结果
        • ans = Math.max(ans, max):比较当前的最大乘积max和已经记录的结果ans,将较大的值赋给ans
    • 最后,将ans转换为int类型并返回,因为结果要求是int类型,而在计算过程中为了避免溢出使用了double类型。

代码实现 

// 乘积最大子数组
// 给你一个整数数组 nums
// 请你找出数组中乘积最大的非空连续子数组
// 并返回该子数组所对应的乘积
// 测试链接 : https://leetcode.cn/problems/maximum-product-subarray/
public class Code01_MaximumProductSubarray {

    // 这节课讲完之后,测试数据又增加了
    // 用int类型的变量会让中间结果溢出
    // 所以改成用double类型的变量
    // 思路是不变的
    public static int maxProduct(int[] nums) {
        double ans = nums[0], min = nums[0], max = nums[0], curmin, curmax;
        for (int i = 1; i < nums.length; i++) {
            curmin = Math.min(nums[i], Math.min(min * nums[i], max * nums[i]));
            curmax = Math.max(nums[i], Math.max(min * nums[i], max * nums[i]));
            min = curmin;
            max = curmax;
            ans = Math.max(ans, max);
        }
        return (int) ans;
    }

}

二.被7整除的子序列最大累加和

题目:被7整除的子序列最大累加和

子序列累加和必须被7整除的最大累加和,给定一个非负数组nums,可以任意选择数字组成子序列,但是子序列的累加和必须被7整除,返回最大累加和。

算法原理

  • 暴力方法(maxSum1函数)原理
    • 整体原理
      • 这种方法是通过穷举数组nums的所有子序列,计算每个子序列的累加和,然后找出其中能被7整除且累加和最大的子序列的累加和。
    • 具体步骤
      • 调用f函数开始计算。
      • f函数中:
        • 递归终止条件:当i == nums.length时,也就是已经遍历完整个数组。此时判断s(累加和)是否能被7整除,如果可以则返回s,否则返回0。
        • 递归计算:对于每个元素nums[i],有两种选择,要么不选择这个元素,调用f(nums, i + 1, s);要么选择这个元素,调用f(nums, i + 1, s + nums[i])。最后返回这两种选择中的较大值。
  • 正式方法(maxSum2函数)原理
    • 整体原理
      • 使用动态规划来解决问题。定义了一个二维数组dp,其中dp[i][j]表示数组nums的前i个数形成的子序列,且子序列累加和对7取模等于j时的最大累加和。通过填充这个二维数组来找到最终的结果。
    • 具体步骤
      • 初始化dp数组:
        • dp[0][0]=0,表示空数组的累加和对7取模为0时的最大累加和为0。
        • 对于j从1到6,dp[0][j]= - 1,表示不存在空数组累加和对7取模为jj不为0)的情况。
      • 填充dp数组:
        • 对于i从1到nn是数组nums的长度):
          • 首先计算x = nums[i - 1]cur = nums[i - 1]%7
          • 对于j从0到6:
            • 先将dp[i][j]初始化为dp[i - 1][j],表示不选择当前元素nums[i - 1]时的最大累加和。
            • 计算needneed的计算是核心。如果cur <= j,则need=(j - cur);否则need=(j - cur + 7)(或者need=(7 + j - cur)%7这种写法也正确)。need表示为了使累加和对7取模后等于j,需要找到前i - 1个数中累加和对7取模等于need的子序列。
            • 如果dp[i - 1][need]!=-1,也就是存在这样的子序列,那么更新dp[i][j]=Math.max(dp[i][j], dp[i - 1][need]+x),即选择当前元素nums[i - 1]时的最大累加和与不选择时的最大累加和中的较大值。
      • 最后,返回dp[n][0],也就是整个数组nums中满足子序列累加和能被7整除的最大累加和。

代码实现

// 子序列累加和必须被7整除的最大累加和
// 给定一个非负数组nums,
// 可以任意选择数字组成子序列,但是子序列的累加和必须被7整除
// 返回最大累加和
// 对数器验证
public class Code02_MaxSumDividedBy7 {

    // 暴力方法
    // 为了验证
    public static int maxSum1(int[] nums) {
        // nums形成的所有子序列的累加和都求出来
        // 其中%7==0的那些累加和中,返回最大的
        // 就是如下f函数的功能
        return f(nums, 0, 0);
    }

    public static int f(int[] nums, int i, int s) {
        if (i == nums.length) {
            return s % 7 == 0 ? s : 0;
        }
        return Math.max(f(nums, i + 1, s), f(nums, i + 1, s + nums[i]));
    }

    // 正式方法
    // 时间复杂度O(n)
    public static int maxSum2(int[] nums) {
        int n = nums.length;
        // dp[i][j] : nums[0...i-1]
        // nums前i个数形成的子序列一定要做到,子序列累加和%7 == j
        // 这样的子序列最大累加和是多少
        // 注意 : dp[i][j] == -1代表不存在这样的子序列
        int[][] dp = new int[n + 1][7];
        dp[0][0] = 0;
        for (int j = 1; j < 7; j++) {
            dp[0][j] = -1;
        }
        for (int i = 1, x, cur, need; i <= n; i++) {
            x = nums[i - 1];
            cur = nums[i - 1] % 7;
            for (int j = 0; j < 7; j++) {
                dp[i][j] = dp[i - 1][j];
                // 这里求need是核心
                need = cur <= j ? (j - cur) : (j - cur + 7);
                // 或者如下这种写法也对
                // need = (7 + j - cur) % 7;
                if (dp[i - 1][need] != -1) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][need] + x);
                }
            }
        }
        return dp[n][0];
    }

    // 为了测试
    // 生成随机数组
    public static int[] randomArray(int n, int v) {
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) {
            ans[i] = (int) (Math.random() * v);
        }
        return ans;
    }

    // 为了测试
    // 对数器
    public static void main(String[] args) {
        int n = 15;
        int v = 30;
        int testTime = 20000;
        System.out.println("测试开始");
        for (int i = 0; i < testTime; i++) {
            int len = (int) (Math.random() * n) + 1;
            int[] nums = randomArray(len, v);
            int ans1 = maxSum1(nums);
            int ans2 = maxSum2(nums);
            if (ans1 != ans2) {
                System.out.println("出错了!");
            }
        }
        System.out.println("测试结束");
    }

}

三.魔法卷轴

题目:魔法卷轴

给定一个数组nums,其中可能有正、负、0 每个魔法卷轴可以把nums中连续的一段全变成0,你希望数组整体的累加和尽可能大,卷轴使不使用、使用多少随意,但一共只有2个魔法卷轴,返回数组尽可能大的累加和。

算法原理

  • 暴力方法(maxSum1函数)原理
    • 整体原理
      • 通过考虑所有可能的魔法卷轴使用情况来找到数组的最大累加和。这里考虑了三种情况:完全不使用魔法卷轴、使用一个魔法卷轴以及使用两个魔法卷轴。
    • 具体步骤
      • 计算完全不使用魔法卷轴的情况(p1):
        • 遍历数组nums,将所有元素累加得到p1
      • 计算使用一个魔法卷轴的情况(p2):
        • 调用mustOneScroll函数,传入整个数组范围(0nums.length - 1),得到在整个数组范围上使用一个魔法卷轴时的最大累加和。
      • 计算使用两个魔法卷轴的情况(p3):
        • 遍历数组,对于每个索引i(从1nums.length - 1),将数组分成两部分(0i - 1inums.length - 1)。
        • 分别调用mustOneScroll函数计算这两部分在使用一个魔法卷轴时的最大累加和,然后将这两个和相加。
        • 通过不断更新p3,找到使用两个魔法卷轴时的最大累加和。
      • 最后,返回p1p2p3中的最大值,即整个数组在使用最多两个魔法卷轴时的最大累加和。
  • mustOneScroll函数原理(暴力计算使用一个魔法卷轴的最大累加和)
    • 整体原理
      • 通过枚举数组nums中所有可能的连续子数组(将其变为0),计算剩余元素的累加和,从而找到在给定范围lr内使用一个魔法卷轴时的最大累加和。
    • 具体步骤
      • 初始化ansInteger.MIN_VALUE,用于记录最大累加和。
      • 双层循环枚举所有可能的子数组范围abl <= a <= b <= r)。
      • 对于每个枚举的子数组范围,计算剩余元素的累加和(将ab范围内的元素变为0后的累加和)。
        • 先计算la - 1范围内元素的累加和,再计算b + 1r范围内元素的累加和,两者相加得到当前的累加和curAns
      • 更新anscurAnsans中的较大值。
      • 最后返回ans,即nums[l...r]范围上使用一个魔法卷轴情况下的最大累加和。
  • 正式方法(maxSum2函数)原理
    • 整体原理
      • 使用动态规划的思想,分别计算完全不使用魔法卷轴、使用一个魔法卷轴(从前向后和从后向前两种情况)以及使用两个魔法卷轴的最大累加和,然后取三者中的最大值。
    • 具体步骤
      • 计算完全不使用魔法卷轴的情况(p1):
        • 遍历数组nums,将所有元素累加得到p1
      • 计算使用一个魔法卷轴的情况(从前向后):
        • 定义prefix数组,prefix[i]表示在0i范围上一定要用1次卷轴的情况下,0i范围上整体最大累加和。
        • 初始化sumnums[0]maxPresumMath.max(0, nums[0])
        • 对于i1nums.length - 1
          • prefix[i] = Math.max(prefix[i - 1]+nums[i], maxPresum),这里考虑了两种情况,一是前一个位置使用卷轴后的最大累加和加上当前元素,二是之前所有前缀和中的最大值(即当前元素之前某个位置使用卷轴后的最大累加和)。
          • 更新sum(前缀和)和maxPresum(之前所有前缀和的最大值)。
        • 最后p2 = prefix[n - 1],即整个数组从前向后使用一个魔法卷轴时的最大累加和。
      • 计算使用一个魔法卷轴的情况(从后向前):
        • 定义suffix数组,suffix[i]表示在in - 1范围上一定要用1次卷轴的情况下,in - 1范围上整体最大累加和。
        • 初始化sumnums[n - 1]maxPresumMath.max(0, sum)
        • 对于in - 20
          • suffix[i] = Math.max(nums[i]+suffix[i + 1], maxPresum),这里考虑了两种情况,一是后一个位置使用卷轴后的最大累加和加上当前元素,二是之后所有后缀和中的最大值(即当前元素之后某个位置使用卷轴后的最大累加和)。
          • 更新sum(后缀和)和maxPresum(之后所有后缀和的最大值)。
      • 计算使用两个魔法卷轴的情况(p3):
        • 遍历数组,对于每个索引i(从1nums.length - 1):
          • 考虑将数组分成两部分(0i - 1in - 1),计算prefix[i - 1]+suffix[i],即前半部分从前向后使用一个魔法卷轴的最大累加和加上后半部分从后向前使用一个魔法卷轴的最大累加和。
          • 通过不断更新p3,找到使用两个魔法卷轴时的最大累加和。
      • 最后,返回p1p2p3中的最大值,即整个数组在使用最多两个魔法卷轴时的最大累加和。

代码实现

// 魔法卷轴
// 给定一个数组nums,其中可能有正、负、0
// 每个魔法卷轴可以把nums中连续的一段全变成0
// 你希望数组整体的累加和尽可能大
// 卷轴使不使用、使用多少随意,但一共只有2个魔法卷轴
// 请返回数组尽可能大的累加和
// 对数器验证
public class Code03_MagicScrollProbelm {

    // 暴力方法
    // 为了测试
    public static int maxSum1(int[] nums) {
        int p1 = 0;
        for (int num : nums) {
            p1 += num;
        }
        int n = nums.length;
        int p2 = mustOneScroll(nums, 0, n - 1);
        int p3 = Integer.MIN_VALUE;
        for (int i = 1; i < n; i++) {
            p3 = Math.max(p3, mustOneScroll(nums, 0, i - 1) + mustOneScroll(nums, i, n - 1));
        }
        return Math.max(p1, Math.max(p2, p3));
    }

    // 暴力方法
    // 为了测试
    // nums[l...r]范围上一定要用一次卷轴情况下的最大累加和
    public static int mustOneScroll(int[] nums, int l, int r) {
        int ans = Integer.MIN_VALUE;
        // l...r范围上包含a...b范围
        // 如果a...b范围上的数字都变成0
        // 返回剩下数字的累加和
        // 所以枚举所有可能的a...b范围
        // 相当暴力,但是正确
        for (int a = l; a <= r; a++) {
            for (int b = a; b <= r; b++) {
                // l...a...b...r
                int curAns = 0;
                for (int i = l; i < a; i++) {
                    curAns += nums[i];
                }
                for (int i = b + 1; i <= r; i++) {
                    curAns += nums[i];
                }
                ans = Math.max(ans, curAns);
            }
        }
        return ans;
    }

    // 正式方法
    // 时间复杂度O(n)
    public static int maxSum2(int[] nums) {
        int n = nums.length;
        if (n == 0) {
            return 0;
        }
        // 情况1 : 完全不使用卷轴
        int p1 = 0;
        for (int num : nums) {
            p1 += num;
        }
        // prefix[i] : 0~i范围上一定要用1次卷轴的情况下,0~i范围上整体最大累加和多少
        int[] prefix = new int[n];
        // 每一步的前缀和
        int sum = nums[0];
        // maxPresum : 之前所有前缀和的最大值
        int maxPresum = Math.max(0, nums[0]);
        for (int i = 1; i < n; i++) {
            prefix[i] = Math.max(prefix[i - 1] + nums[i], maxPresum);
            sum += nums[i];
            maxPresum = Math.max(maxPresum, sum);
        }
        // 情况二 : 必须用1次卷轴
        int p2 = prefix[n - 1];
        // suffix[i] : i~n-1范围上一定要用1次卷轴的情况下,i~n-1范围上整体最大累加和多少
        int[] suffix = new int[n];
        sum = nums[n - 1];
        maxPresum = Math.max(0, sum);
        for (int i = n - 2; i >= 0; i--) {
            suffix[i] = Math.max(nums[i] + suffix[i + 1], maxPresum);
            sum += nums[i];
            maxPresum = Math.max(maxPresum, sum);
        }
        // 情况二 : 必须用2次卷轴
        int p3 = Integer.MIN_VALUE;
        for (int i = 1; i < n; i++) {
            // 枚举所有的划分点i
            // 0~i-1 左
            // i~n-1 右
            p3 = Math.max(p3, prefix[i - 1] + suffix[i]);
        }
        return Math.max(p1, Math.max(p2, p3));
    }

    // 为了测试
    public static int[] randomArray(int n, int v) {
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) {
            ans[i] = (int) (Math.random() * (v * 2 + 1)) - v;
        }
        return ans;
    }

    // 为了测试
    public static void main(String[] args) {
        int n = 50;
        int v = 100;
        int testTime = 10000;
        System.out.println("测试开始");
        for (int i = 0; i < testTime; i++) {
            int len = (int) (Math.random() * n);
            int[] nums = randomArray(len, v);
            int ans1 = maxSum1(nums);
            int ans2 = maxSum2(nums);
            if (ans1 != ans2) {
                System.out.println("出错了!");
            }
        }
        System.out.println("测试结束");
    }

}

四.三个无重叠子数组的最大和

题目:三个无重叠子数组的最大和

给你一个整数数组 nums 和一个整数 k 找出三个长度为 k 、互不重叠、且全部数字和(3 * k 项)最大的子数组,并返回这三个子数组,以下标的数组形式返回结果,数组中的每一项分别指示每个子数组的起始位置,如果有多个结果,返回字典序最小的一个。

算法原理

  • 整体原理
    • 为了找出三个长度为(k)、互不重叠且和最大的子数组,首先计算以每个位置开头且长度为(k)的子数组的累加和。然后分别从左到右和从右到左找出在一定范围内拥有最大累加和的子数组的起始位置。最后通过枚举中间子数组的起始位置,结合左右两边的最大累加和子数组的起始位置,找到三个子数组和最大的情况。
  • 具体步骤
    • 计算以每个位置开头且长度为(k)的子数组的累加和(sums数组):
      • 使用滑动窗口的思想。初始化l = 0r = 0sum = 0
      • 随着r从(0)到n - 1遍历数组:
        • 不断将nums[r]累加到sum中。
        • r - l+1 = k时,说明已经形成了一个长度为(k)的子数组,此时将sum赋值给sums[l],然后将nums[l]sum中减去,并将l向后移动一位。
    • 计算prefix数组:
      • prefix[i]表示在(0)到(i)范围上所有长度为(k)的子数组中,拥有最大累加和的子数组的起始位置。
      • 从(l = 1),(r = k)开始,随着(lr的同步移动(r从k到n - 1):
        • 如果sums[l]>sums[prefix[r - 1]],为了在同样最大累加和的情况下得到最小的字典序,将prefix[r]设置为l;否则prefix[r]等于prefix[r - 1]
    • 计算suffix数组:
      • suffix[i]表示在(i)到n - 1范围上所有长度为(k)的子数组中,拥有最大累加和的子数组的起始位置。
      • 首先将suffix[n - k]=n - k
      • 然后从(l = n - k - 1开始,向0方向遍历:
        • 如果sums[l]>=sums[suffix[l + 1]],为了在同样最大累加和的情况下得到最小的字典序,将suffix[l]设置为l;否则suffix[l]等于suffix[l + 1]
    • 枚举中间子数组的起始位置找到最大和的三个子数组:
      • 设三个子数组的起始位置分别为(a)、(b)、(c),最大和为max
      • 按照0...i - 1(左)、i...j(中,长度为(k))、j + 1...n - 1(右)的划分方式,其中(i = k),(j = 2 * k - 1)开始,随着(ij的同步移动(j从2 * k - 1n - k - 1):
        • 找到左边范围((0)到(i - 1))内最大累加和子数组的起始位置(p = prefix[i - 1]),中间子数组起始位置(i),右边范围((j+1)到(n - 1))内最大累加和子数组的起始位置(s = suffix[j + 1])。
        • 计算三个子数组的累加和sum = sums[p]+sums[i]+sums[s]
        • 如果sum>max,为了在同样最大累加和的情况下得到最小的字典序,更新max、(a)、(b)、(c)的值,分别为sum、(p)、(i)、(s)。
      • 最后返回new int[]{a, b, c},即为三个长度为(k)、互不重叠且和最大的子数组的起始位置数组。

代码实现

// 三个无重叠子数组的最大和
// 给你一个整数数组 nums 和一个整数 k
// 找出三个长度为 k 、互不重叠、且全部数字和(3 * k 项)最大的子数组
// 并返回这三个子数组
// 以下标的数组形式返回结果,数组中的每一项分别指示每个子数组的起始位置
// 如果有多个结果,返回字典序最小的一个
// 测试链接 : https://leetcode.cn/problems/maximum-sum-of-3-non-overlapping-subarrays/
public class Code04_MaximumSum3UnoverlappingSubarrays {

    public static int[] maxSumOfThreeSubarrays(int[] nums, int k) {
        int n = nums.length;
        // sums[i] : 以i开头并且长度为k的子数组的累加和
        int[] sums = new int[n];
        for (int l = 0, r = 0, sum = 0; r < n; r++) {
            // l....r
            sum += nums[r];
            if (r - l + 1 == k) {
                sums[l] = sum;
                sum -= nums[l];
                l++;
            }
        }
        // prefix[i] :
        // 0~i范围上所有长度为k的子数组中,拥有最大累加和的子数组,是以什么位置开头的
        int[] prefix = new int[n];
        for (int l = 1, r = k; r < n; l++, r++) {
            if (sums[l] > sums[prefix[r - 1]]) {
                // 注意>,为了同样最大累加和的情况下,最小的字典序
                prefix[r] = l;
            } else {
                prefix[r] = prefix[r - 1];
            }
        }
        // suffix[i] :
        // i~n-1范围上所有长度为k的子数组中,拥有最大累加和的子数组,是以什么位置开头的
        int[] suffix = new int[n];
        suffix[n - k] = n - k;
        for (int l = n - k - 1; l >= 0; l--) {
            if (sums[l] >= sums[suffix[l + 1]]) {
                // 注意>=,为了同样最大累加和的情况下,最小的字典序
                suffix[l] = l;
            } else {
                suffix[l] = suffix[l + 1];
            }
        }
        int a = 0, b = 0, c = 0, max = 0;
        // 0...i-1    i...j    j+1...n-1
        //   左     中(长度为k)     右
        for (int p, s, i = k, j = 2 * k - 1, sum; j < n - k; i++, j++) {
            // 0.....i-1   i.....j  j+1.....n-1
            // 最好开头p      i开头     最好开头s
            p = prefix[i - 1];
            s = suffix[j + 1];
            sum = sums[p] + sums[i] + sums[s];
            if (sum > max) {
                // 注意>,为了同样最大累加和的情况下,最小的字典序
                max = sum;
                a = p;
                b = i;
                c = s;
            }
        }
        return new int[] { a, b, c };
    }

}

五.可以翻转1次的情况下子数组最大累加和

题目:可以翻转1次的情况下子数组最大累加和

给定一个数组nums,现在允许你随意选择数组连续一段进行翻转,也就是子数组逆序的调整。比如翻转[1,2,3,4,5,6]的[2~4]范围,得到的是[1,2,5,4,3,6] 返回必须随意翻转1次之后,子数组的最大累加和。

算法原理

  • 暴力方法(maxSumReverse1函数)原理
    • 整体原理
      • 通过枚举数组nums中所有可能的子数组,并对每个子数组进行翻转操作,然后计算翻转后的数组的最大子数组累加和,最后取这些最大累加和中的最大值。
    • 具体步骤
      • 初始化ansInteger.MIN_VALUE,用于记录最终的最大累加和。
      • 双层循环枚举子数组的左右边界lrl从(0)到nums.length - 1rlnums.length - 1)。
        • 对于每个子数组nums[l...r],先调用reverse函数进行翻转。
        • 然后调用maxSum函数计算翻转后数组的最大子数组累加和,并更新ansans和当前最大累加和中的较大值。
        • 再调用reverse函数将子数组翻转回原来的顺序,以便进行下一轮枚举。
      • 最后返回ans,即经过一次翻转后子数组的最大累加和。
  • reverse函数原理(翻转子数组)
    • 整体原理
      • 使用双指针法,通过交换子数组两端的元素,逐步向中间靠拢,实现子数组的逆序调整。
    • 具体步骤
      • l < r时:
        • 先将nums[l]暂存到tmp变量中。
        • 然后将nums[r]赋值给nums[l],并将l向后移动一位。
        • 再将tmp赋值给nums[r],并将r向前移动一位。
      • 这样就完成了nums[l...r]范围内元素的逆序调整。
  • maxSum函数原理(计算子数组最大累加和)
    • 整体原理
      • 使用动态规划的思想,以线性时间复杂度计算数组的最大子数组累加和。
    • 具体步骤
      • 初始化ansnums[0]prenums[0]
      • i = 1nums.length - 1遍历数组:
        • 计算pre = Math.max(nums[i], pre + nums[i]),这里考虑了两种情况,一是当前元素nums[i]本身,二是包含当前元素的前面子数组的累加和pre + nums[i],取两者中的较大值。
        • 更新ans = Math.max(ans, pre),即比较当前的最大累加和pre和已经记录的ans,取较大值。
      • 最后返回ans,即数组的最大子数组累加和。
  • 正式方法(maxSumReverse2函数)原理
    • 整体原理
      • 通过分别计算以每个位置开头的子数组的最大累加和(start数组),以及从左到右的最大累加和(maxEnd),并枚举中间的划分点,计算在可以翻转一次的情况下子数组的最大累加和。
    • 具体步骤
      • 计算start数组:
        • start[i]表示所有必须以i开头的子数组中最大累加和是多少。
        • 初始化start[n - 1]=nums[n - 1]
        • i = n - 2到(0)反向遍历数组:
          • 计算start[i]=Math.max(nums[i], nums[i]+start[i + 1]),这里考虑了两种情况,一是当前元素nums[i]本身,二是当前元素加上以i + 1开头的子数组的最大累加和nums[i]+start[i + 1],取两者中的较大值。
      • 初始化ans = start[0]end = nums[0]maxEnd = nums[0]
      • i = 1nums.length - 1遍历数组:
        • 计算ans = Math.max(ans, maxEnd + start[i]),这里通过枚举划分点(i),将以(0)到(i - 1)范围内某个位置结尾的最大累加和(maxEnd)与以(i)开头的最大累加和(start[i])相加,并更新ans为较大值。
        • 计算end = Math.max(nums[i], end + nums[i]),这里考虑了两种情况,一是当前元素nums[i]本身,二是包含当前元素的前面子数组的累加和end + nums[i],取两者中的较大值,得到以(i)结尾的子数组的最大累加和。
        • 更新maxEnd = Math.max(maxEnd, end),即比较当前以(i)结尾的最大累加和end和已经记录的maxEnd,取较大值。
      • 最后更新ans = Math.max(ans, maxEnd),考虑不进行翻转的情况(即最大累加和就在原始数组的某个子数组中),取ansmaxEnd中的较大值并返回。

代码实现

// 可以翻转1次的情况下子数组最大累加和
// 给定一个数组nums,
// 现在允许你随意选择数组连续一段进行翻转,也就是子数组逆序的调整
// 比如翻转[1,2,3,4,5,6]的[2~4]范围,得到的是[1,2,5,4,3,6]
// 返回必须随意翻转1次之后,子数组的最大累加和
// 对数器验证
public class Code05_ReverseArraySubarrayMaxSum {

    // 暴力方法
    // 为了验证
    public static int maxSumReverse1(int[] nums) {
        int ans = Integer.MIN_VALUE;
        for (int l = 0; l < nums.length; l++) {
            for (int r = l; r < nums.length; r++) {
                reverse(nums, l, r);
                ans = Math.max(ans, maxSum(nums));
                reverse(nums, l, r);
            }
        }
        return ans;
    }

    // nums[l...r]范围上的数字进行逆序调整
    public static void reverse(int[] nums, int l, int r) {
        while (l < r) {
            int tmp = nums[l];
            nums[l++] = nums[r];
            nums[r--] = tmp;
        }
    }

    // 返回子数组最大累加和
    public static int maxSum(int[] nums) {
        int n = nums.length;
        int ans = nums[0];
        for (int i = 1, pre = nums[0]; i < n; i++) {
            pre = Math.max(nums[i], pre + nums[i]);
            ans = Math.max(ans, pre);
        }
        return ans;
    }

    // 正式方法
    // 时间复杂度O(n)
    public static int maxSumReverse2(int[] nums) {
        int n = nums.length;
        // start[i] : 所有必须以i开头的子数组中,最大累加和是多少
        int[] start = new int[n];
        start[n - 1] = nums[n - 1];
        for (int i = n - 2; i >= 0; i--) {
            // nums[i]
            // nums[i] + start[i+1]
            start[i] = Math.max(nums[i], nums[i] + start[i + 1]);
        }
        int ans = start[0];
        // end : 子数组必须以i-1结尾,其中的最大累加和
        int end = nums[0];
        // maxEnd :
        // 0~i-1范围上,
        // 子数组必须以0结尾,其中的最大累加和
        // 子数组必须以1结尾,其中的最大累加和
        // ...
        // 子数组必须以i-1结尾,其中的最大累加和
        // 所有情况中,最大的那个累加和就是maxEnd
        int maxEnd = nums[0];
        for (int i = 1; i < n; i++) {
            // maxend   i....
            // 枚举划分点 i...
            ans = Math.max(ans, maxEnd + start[i]);
            // 子数组必须以i结尾,其中的最大累加和
            end = Math.max(nums[i], end + nums[i]);
            maxEnd = Math.max(maxEnd, end);
        }
        ans = Math.max(ans, maxEnd);
        return ans;
    }

    // 为了测试
    // 生成随机数组
    public static int[] randomArray(int n, int v) {
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) {
            ans[i] = (int) (Math.random() * (v * 2 + 1)) - v;
        }
        return ans;
    }

    // 为了测试
    // 对数器
    public static void main(String[] args) {
        int n = 50;
        int v = 200;
        int testTime = 20000;
        System.out.println("测试开始");
        for (int i = 0; i < testTime; i++) {
            int len = (int) (Math.random() * n) + 1;
            int[] arr = randomArray(len, v);
            int ans1 = maxSumReverse1(arr);
            int ans2 = maxSumReverse2(arr);
            if (ans1 != ans2) {
                System.out.println("出错了!");
            }
        }
        System.out.println("测试结束");
    }

}

六.删掉1个数字后长度为k的子数组最大累加和

题目:删掉1个数字后长度为k的子数组最大累加和

给定一个数组nums,求必须删除一个数字后的新数组中,长度为k的子数组最大累加和,删除哪个数字随意。

算法原理

  • 暴力方法(maxSum1函数)原理
    • 整体原理
      • 通过枚举数组nums中每个元素作为要删除的元素,然后在删除该元素后的新数组中找到长度为(k)的子数组的最大累加和,最后取这些最大累加和中的最大值。
    • 具体步骤
      • 首先判断如果数组长度(n\leq k),直接返回(0)。
      • 初始化ansInteger.MIN_VALUE,用于记录最终的最大累加和。
      • 遍历数组nums中的每个元素(索引(i)从(0)到(n - 1)):
        • 调用delete函数删除索引为(i)的元素,得到新数组rest
        • 调用lenKmaxSum函数计算新数组rest中长度为(k)的子数组的最大累加和,并更新ansans和当前最大累加和中的较大值。
      • 最后返回ans,即经过删除一个元素后长度为(k)的子数组最大累加和。
  • delete函数原理(删除指定索引元素)
    • 整体原理
      • 创建一个新数组,将原数组中除了指定索引位置的元素依次复制到新数组中,从而实现删除指定索引元素的功能。
    • 具体步骤
      • 计算新数组的长度(len=nums.length - 1),并创建长度为(len)的新数组ans
      • 使用两个指针(i = 0)和(j = 0),其中(j)用于遍历原数组,(i)用于填充新数组。
      • 当(j)遍历原数组时,如果(j\neq index),则将(nums[j])赋值给(ans[i]),并将(i)和(j)分别向后移动一位;如果(j = index),则只将(j)向后移动一位,不向新数组赋值。
      • 最后返回新数组ans
  • lenKmaxSum函数原理(计算新数组长度为(k)子数组最大累加和)
    • 整体原理
      • 通过枚举新数组中所有长度为(k)的子数组,计算每个子数组的累加和,最后取这些累加和中的最大值。
    • 具体步骤
      • 初始化ansInteger.MIN_VALUE,用于记录最终的最大累加和。
      • 外层循环从(i = 0)到(n - k),用于确定子数组的起始位置。
      • 对于每个起始位置(i),初始化(cur = 0),然后内层循环从(j = i)开始,计数(cnt)从(0)开始,当(cnt < k)时:
        • 将(nums[j])累加到cur中,并将j和cnt分别向后移动一位。
      • 每次内层循环结束后,更新ans = Math.max(ans, cur),即比较当前子数组的累加和(cur和已经记录的ans),取较大值。
      • 最后返回ans
  • 正式方法(maxSum2函数)原理
    • 整体原理
      • 使用单调队列和滑动窗口的思想来优化计算。通过维护一个单调队列来找到窗口内的最小值,从而高效地计算在删除一个元素后的数组中长度为(k)的子数组的最大累加和。
    • 具体步骤
      • 首先判断如果数组长度(n\leq k),直接返回(0)。
      • 创建一个用于单调队列的数组window,并初始化窗口的左右指针(l = 0),(r = 0),以及窗口累加和(sum = 0),结果ans = Integer.MIN_VALUE
      • 遍历数组nums中的每个元素(索引(i)从(0)到(n - 1)):
        • 在单调队列操作中,当(l < r)且nums[window[r - 1]]>=nums[i]时,将(r)减(1),以保持队列的单调性(单调递增,这里存储的是索引,对应的值是单调递增的)。
        • 将当前元素的索引(i)加入单调队列(window[r++]=i)。
        • 将当前元素的值累加到窗口累加和(sum中(sum += nums[i]`)。
        • 当(i >= k)时:
          • 计算当前可能的最大累加和ans = Math.max(ans, (int)(sum - nums[window[l]])),这里用窗口累加和(sum减去单调队列头部(最小元素)对应的元素值(nums[window[l]],得到一个可能的最大累加和,并更新ans
          • 如果单调队列头部的索引(window[l])等于(i - k),说明该元素已经不在当前长度为(k)的窗口内,将(l)加(1),从队列中弹出该元素。
          • 从窗口累加和(sum中减去(nums[i - k]),因为窗口向右移动了一位。
      • 最后返回ans,即经过删除一个元素后长度为(k)的子数组最大累加和。

代码实现

// 删掉1个数字后长度为k的子数组最大累加和
// 给定一个数组nums,求必须删除一个数字后的新数组中
// 长度为k的子数组最大累加和,删除哪个数字随意
// 对数器验证
public class Code06_DeleteOneNumberLengthKMaxSum {

    // 暴力方法
    // 为了测试
    public static int maxSum1(int[] nums, int k) {
        int n = nums.length;
        if (n <= k) {
            return 0;
        }
        int ans = Integer.MIN_VALUE;
        for (int i = 0; i < n; i++) {
            int[] rest = delete(nums, i);
            ans = Math.max(ans, lenKmaxSum(rest, k));
        }
        return ans;
    }

    // 暴力方法
    // 为了测试
    // 删掉index位置的元素,然后返回新数组
    public static int[] delete(int[] nums, int index) {
        int len = nums.length - 1;
        int[] ans = new int[len];
        int i = 0;
        for (int j = 0; j < nums.length; j++) {
            if (j != index) {
                ans[i++] = nums[j];
            }
        }
        return ans;
    }

    // 暴力方法
    // 为了测试
    // 枚举每一个子数组找到最大累加和
    public static int lenKmaxSum(int[] nums, int k) {
        int n = nums.length;
        int ans = Integer.MIN_VALUE;
        for (int i = 0; i <= n - k; i++) {
            int cur = 0;
            for (int j = i, cnt = 0; cnt < k; j++, cnt++) {
                cur += nums[j];
            }
            ans = Math.max(ans, cur);
        }
        return ans;
    }

    // 正式方法
    // 时间复杂度O(N)
    public static int maxSum2(int[] nums, int k) {
        int n = nums.length;
        if (n <= k) {
            return 0;
        }
        // 单调队列 : 维持窗口内最小值的更新结构,讲解054的内容
        int[] window = new int[n];
        int l = 0;
        int r = 0;
        // 窗口累加和
        long sum = 0;
        int ans = Integer.MIN_VALUE;
        for (int i = 0; i < n; i++) {
            // 单调队列 : i位置进入单调队列
            while (l < r && nums[window[r - 1]] >= nums[i]) {
                r--;
            }
            window[r++] = i;
            sum += nums[i];
            if (i >= k) {
                ans = Math.max(ans, (int) (sum - nums[window[l]]));
                if (window[l] == i - k) {
                    // 单调队列 : 如果单调队列最左侧的位置过期了,从队列中弹出
                    l++;
                }
                sum -= nums[i - k];
            }
        }
        return ans;
    }

    // 为了测试
    // 生成长度为n,值在[-v, +v]之间的随机数组
    public static int[] randomArray(int n, int v) {
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) {
            ans[i] = (int) (Math.random() * (2 * v + 1)) - v;
        }
        return ans;
    }

    // 为了测试
    // 对数器
    public static void main(String[] args) {
        int n = 200;
        int v = 1000;
        int testTimes = 10000;
        System.out.println("测试开始");
        for (int i = 0; i < testTimes; i++) {
            int len = (int) (Math.random() * n) + 1;
            int[] nums = randomArray(len, v);
            int k = (int) (Math.random() * n) + 1;
            int ans1 = maxSum1(nums, k);
            int ans2 = maxSum2(nums, k);
            if (ans1 != ans2) {
                System.out.println("出错了!");
            }
        }
        System.out.println("测试结束");
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值