动态规划之最大子数组和模型

最大子数组和模型

在这里插入图片描述

今天我们来学习「最大子数字和模型」问题,它又被称为「最大子段和」问题,可以将问题抽象为:给定一个整型数组,请求出该数组一段连续非空的子数组,使得这个子数组的和最大。

可以通过前缀和来解决这个问题,具体来说,设置一个变量curr,用来保存某一段子数组的和,当curr + nums[i]的值比nums[i]本身还要小的时候,说明curr目前是一个比nums[i]还小的负数,用它继续来保存答案不会使当前子数组的和更大,因此更新curr的值为nums[i],开始统计新的子数组和。

需要注意的是,同样可以使用「动态规划」来解决这类问题。定义状态f[i]表示以nums[i]结尾的最大子数组和,子数组和最大值的状态转移方程就是f[i] = max(f[i - 1], 0) + nums[i]。不难看出,前缀和的解法就是动态规划解法的空间优化版本。

LeetCode 53. 最大子数组和

思路

这道题就是模版题,直接用开头讲解的思路解决这道题即可。

Golang 代码

func maxSubArray(nums []int) int {
    n := len(nums)
    curr, ans := nums[0], nums[0]
    for i := 1; i < n; i ++ {
        curr = max(curr, 0) + nums[i]
        ans = max(ans, curr)
    }
    return ans
}

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

思路

这道题的解题思路与「最大子数组和」差不多。具体来说,我们需要维护一个字母与开销的对应关系,所以首先开一个长度为 26 的整型数组。如果字母出现在了chars数组当中,意味着它的开销保存在 vals 当中,否则就是1~26当中的某个值。

维护好这个关系之后,接下来对字符串s应用最大子数组相同的算法,即可解决这个问题。

Golang 代码

func maximumCostSubstring(s string, chars string, vals []int) int {
    costs := [26]int{}
    for i := 1; i <= 26; i ++ {
        costs[i - 1] = i
    }
    for i, c := range chars {
        costs[c - 'a'] = vals[i]
    }
    curr, ans, n := 0, 0, len(s)
    for i := 0; i < n; i ++ {
        curr = max(curr, 0) + costs[s[i] - 'a']
        ans = max(ans, curr)
    }
    return ans
}

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

思路

这道题目的思路很简单,本质上就是「最大子数组和」问题,但是需要同时统计「最小子数组和」,原因是最小子数组和如果是负数,取绝对值之后的值可能大于最大子数组和,这个思路与 LeetCode 152. 乘积最大子数组 非常相似。

Golang 代码

func maxAbsoluteSum(nums []int) int {
    n := len(nums)
    currMin, currMax, ans := 0, 0, 0
    for i := 0; i < n; i ++ {
        currMin = min(currMin + nums[i], nums[i])
        currMax = max(currMax + nums[i], nums[i])
        ans = max(ans, -currMin, currMax)
    }
    return ans
}

LeetCode 1191. K 次串联后最大子数组之和

思路

这道题目有一定的难度,大致题意就是将原数组nums拼接k次后,新的「最大子数组和」是多少。

如果 k = = 1 k == 1 k==1,那么这个问题退化为「最大子数组和」问题,解题思路与母题相同。

如果 k = = 2 k == 2 k==2,相当于将两个数组拼接起来,仍然可以直接对拼接后的数组使用「最大子数组和」问题的解法进行解决,但是此时最优解对应的子数组可能会横跨两个数组。

k ≥ 3 k \geq 3 k3时,问题变得有趣了起来。我们先将一个数组固定在左侧,一个数组固定在右侧,那么中间就有k - 2个数组。如果nums数组的和是正的,那么最优解一定会覆盖所以k - 2个数组。反之如果nums数组的和是负的,那么最优解要么出现在单个子数组当中,要么最多只横跨两个子数组,即出现在数组相邻的情况下。

知道了上述结论,我们就可以开始解题了。具体来说,单独考虑k == 1的情况,然后开始考虑$k \geq2 的情况,首先求出 的情况,首先求出 的情况,首先求出k==2$时的最优解ans,同时统计nums数组的和arrSum,如果和大于 0,那么最优解可能是ans + arrSum * (k - 2)

Golang 代码

func solve(nums []int) (int, int) {
    curr, ans, arrSum := 0, 0, 0
    for i := 0; i < len(nums); i ++ {
        arrSum += nums[i]
        curr = max(curr, 0) + nums[i]
        ans = max(curr, ans)
    }
    return ans, arrSum
}

func kConcatenationMaxSum(arr []int, k int) int {
    const MOD int = 1e9 + 7
    ans, arrSum := solve(arr)
    if k == 1 {
        return ans
    }
    ans, _ = solve(append(arr, arr...))
    if arrSum > 0 {
        ans = max(ans, 0) + arrSum * (k - 2) % MOD
    }
    return ans % MOD
}

LeetCode 918. 环形子数组的最大和

思路

这道题目的答案有两种情况,第一种情况就是最优解出现在顺序序列当中,不会形成环状,这种情况下答案可以通过「最大子数组和问题的解法」得到。

第二种情况是最优解一部分在顺序序列左侧,一部分在右侧,通过环状连接起来,得到最终的答案。这种情况需要额外考虑,其实就是将左侧的数组最大值与右侧的数组最大值相加。

详细的实现请见下面的代码。

Golang 代码

func maxSubarraySumCircular(nums []int) int {
    n := len(nums)
    leftMax := make([]int, n)
    curr, ans, leftSum := nums[0], nums[0], nums[0]
    leftMax[0] = nums[0]
    for i := 1; i < n; i ++ {
        curr = max(curr + nums[i], nums[i])
        ans = max(ans, curr)
        leftSum += nums[i]
        leftMax[i] = max(leftMax[i - 1], leftSum)
    }
    rightSum := 0
    for i := n - 1; i >= 1; i -- {
        rightSum += nums[i]
        ans = max(ans, leftMax[i - 1] + rightSum)
    }
    return ans
}

LeetCode 2321. 拼接数组的最大分数

思路

这是一道 Hard,在解这道题目的时候,需要运用到一定的数学。如果不进行子数组交换,那么数组的和就是 a r r S u m = ∑ n i = 0 n u m s [ i ] arrSum = \sum^{i=0}_nnums[i] arrSum=ni=0nums[i]。而如果需要进行子数组交换,假设要交换的区间是 [ l e f t , r i g h t ] [left, right] [left,right]且区间合法,假定原数组为nums1,交换的数组为nums2,那么nums1交换后可能的数组和就是 a r r S u m = ∑ n i = 0 n u m s [ i ] − ∑ r i g h t i = l e f t n u m s [ i ] + ∑ r i g h t i = l e f t n u m s 2 [ i ] arrSum=\sum^{i=0}_nnums[i]-\sum^{i=left}_{right}nums[i]+\sum^{i=left}_{right}nums2[i] arrSum=ni=0nums[i]righti=leftnums[i]+righti=leftnums2[i]。合并后面两项,得到 a r r S u m = ∑ n i = 0 n u m s [ i ] + ∑ r i g h t i = l e f t ( n u m s 2 [ i ] − n u m s 1 [ i ] ) arrSum=\sum^{i=0}_nnums[i]+\sum^{i=left}_{right}(nums2[i]-nums1[i]) arrSum=ni=0nums[i]+righti=left(nums2[i]nums1[i])

所以我们只需要将 ∑ r i g h t i = l e f t ( n u m s 2 [ i ] − n u m s 1 [ i ] ) \sum^{i=left}_{right}(nums2[i]-nums1[i]) righti=left(nums2[i]nums1[i])这个区间的值diff最大化,即可得到最终的答案。在求解maxDiff的时候,可以使用「最大子数组和问题的解法」来统计答案。

还有一个要注意的点是,题目中给定了两个数组,这两个数组都可以作为nums1,所以需要统计两次答案,具体详见下面的代码。

Golang 代码

func solve(nums1, nums2 []int) int {
    n, arrSum, currDiff, maxDiff := len(nums1), 0, 0, 0
    for i := 0; i < n; i ++ {
        arrSum += nums1[i]
        currDiff = max(currDiff, 0) + nums2[i] - nums1[i]
        maxDiff = max(currDiff, maxDiff)
    }
    return arrSum + maxDiff
}

func maximumsSplicedArray(nums1 []int, nums2 []int) int {
    return max(solve(nums1, nums2), solve(nums2, nums1))
}

LeetCode 152. 乘积的最大子数组

思路

这道题目被灵神收录为「最大子数组和问题」的思维拓展题,实际上这道题就是将「和」的问题转换为了「积」的问题。

需要重点考虑的一点就是,如果两个负数相乘,得到的是正数,因此整个子数组乘积的最小值与一个负数相乘,得到的结果可能比当前的最大值还要大,考虑到这一点就可以来解决这个问题了。

Golang 代码

func maxProduct(nums []int) int {
    n := len(nums)
    maxSum := make([]int, n)
    minSum := make([]int, n)
    maxSum[0], minSum[0] = nums[0], nums[0]
    ans := nums[0]
    for i := 1; i < n; i ++ {
        maxSum[i] = max(nums[i], nums[i] * maxSum[i - 1], nums[i] * minSum[i - 1])
        minSum[i] = min(nums[i], nums[i] * maxSum[i - 1], nums[i] * minSum[i - 1])
        ans = max(ans, maxSum[i], minSum[i])
    }
    return ans
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值