最大子数组和问题的变种——最多允许翻转一个子数组的最大子数组和
题目描述
给定一个整数数组nums,你可以选择翻转数组中的任意一个连续子数组(即将该子数组的元素顺序反转),然后返回可能得到的最大子数组和。注意,你最多只能进行一次翻转操作,也可以选择不翻转。
示例:
输入:nums = [1, -2, 3, -2, 4]
输出:8
解释:翻转子数组[-2, 3, -2]得到[1, 3, -2, 3, 4],最大子数组和为3 + (-2) + 3 + 4 = 8
解题过程
这个问题可以分解为几个关键步骤,我们需要分别处理不翻转和翻转一次的情况。
1. 基础情况:不翻转
首先,我们需要计算原数组的最大子数组和。这可以通过经典的Kadane算法实现:
- 定义dp[i]为以第i个元素结尾的最大子数组和
- 状态转移方程:dp[i] = max(nums[i], dp[i-1] + nums[i])
- 最终结果是所有dp[i]中的最大值
2. 翻转一次的情况分析
当我们翻转一个子数组nums[i...j]时,实际上相当于:
- 保持nums[0...i-1]不变
- 将nums[i...j]反转
- 保持nums[j+1...n-1]不变
关键观察:翻转nums[i...j]后,最大子数组和可能出现在:
- 完全在翻转区间内
- 跨越翻转区间边界
- 完全不涉及翻转区间
但更有效的方法是考虑翻转操作对最大子数组和的影响。
3. 预处理数组
为了高效处理翻转情况,我们需要预处理四个数组:
left_max[i]:表示从数组开头到位置i(包括i)的最大子数组和
- 计算方法:从左到右应用Kadane算法,记录每个位置的最大值
right_max[i]:表示从位置i到数组末尾的最大子数组和
- 计算方法:从右到左应用Kadane算法
left_sum[i]:表示从开头到位置i的数组元素和
- 用于快速计算区间和
right_sum[i]:表示从位置i到末尾的数组元素和
4. 翻转情况的最大子数组和计算
对于每个可能的分割点i,考虑翻转操作跨越i的情况:
- 翻转区间为[k, i-1],其中k < i
- 翻转后的最大子数组和 = left_max[k] + (right_sum[i] - left_sum[i-1]) + 某些项
更精确的公式:
对于每个位置i,最大可能和 = left_max[i] + max(0, 某个右侧最大和)
5. 完整算法步骤
- 计算不翻转时的最大子数组和(基础情况)
- 预处理left_max, right_max, left_sum, right_sum数组
- 对于每个位置i,计算:
- 以i为分割点的最大可能和
- 考虑翻转区间完全在左侧、完全在右侧、跨越i的情况
- 取所有情况的最大值
6. 时间复杂度优化
直接枚举所有可能的翻转区间是O(n²)的,我们需要优化到O(n)。
关键技巧:同时维护多个状态变量,在一次遍历中计算所有可能情况。
7. 最终解决方案
定义:
- no_rev:不翻转时的最大子数组和
- max_left[i]:从0到i的最大子数组和
- max_right[i]:从i到n-1的最大子数组和
- total_left[i]:从0到i的数组元素和
- total_right[i]:从i到n-1的数组元素和
对于每个位置i,考虑翻转区间结束于i-1的情况:
翻转后的最大和 = max_left[i] + max(0, max_right[i] - (total_right[i] - total_left[i-1]))
8. 完整代码框架
function maxSubarrayAfterFlip(nums):
n = len(nums)
if n == 0: return 0
# 基础情况:不翻转
max_no_flip = kadane(nums)
# 预处理数组
left_max = [0] * n
right_max = [0] * n
left_sum = [0] * n
right_sum = [0] * n
# 填充预处理数组...
# 计算翻转一次的最大值
max_with_flip = -float('inf')
for i in range(n):
# 计算以i为关键点的各种翻转情况
# 更新max_with_flip
return max(max_no_flip, max_with_flip)
9. 边界情况处理
- 空数组:返回0
- 全负数数组:返回最大单个元素
- 全正数数组:返回数组总和
这个问题的核心在于将翻转操作转化为对数组分段的重新组合,通过预处理和状态维护将时间复杂度优化到O(n)。