文章目录
最大子数组和模型
今天我们来学习「最大子数字和模型」问题,它又被称为「最大子段和」问题,可以将问题抽象为:给定一个整型数组,请求出该数组一段连续非空的子数组,使得这个子数组的和最大。
可以通过前缀和来解决这个问题,具体来说,设置一个变量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
k≥3时,问题变得有趣了起来。我们先将一个数组固定在左侧,一个数组固定在右侧,那么中间就有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
}