最大子段和的变种:允许至多删除一个元素的最大子段和
题目描述
给定一个整数数组 nums,你需要求出其最大子段和。这里子段指的是数组中连续的一段非空子数组。不过,与经典的最大子段和问题不同,本题允许你至多删除一个元素(也可以不删除)来得到总和最大的子段。你需要计算在所有可能的删除情况(包括不删除)下,能获得的最大子段和。
例如:
- 输入:
nums = [1, -2, 0, 3] - 输出:
4 - 解释:可以选择子段
[1, -2, 0, 3]并删除-2,得到[1, 0, 3],其和为4。不删除任何元素时,最大子段和为3(子段[3]或[0, 3]),删除后能获得更大和。
解题思路
这是一个经典的线性动态规划变种问题。核心在于,当我们允许删除一个元素时,实际上是允许子段在某个位置“跳过”一个元素,但子段其余部分必须连续。我们可以通过定义两个状态数组来递推计算。
步骤详解
-
定义状态
设dp0[i]表示以第i个元素结尾,且没有删除任何元素的最大子段和。
设dp1[i]表示以第i个元素结尾,且已经删除了一个元素的最大子段和。
这里“以第 i 个元素结尾”意味着子段必须包含nums[i],这样我们才能方便地递推。 -
状态转移方程
-
对于
dp0[i](没有删除过元素):
它要么是只包含当前元素nums[i],要么是接在前一个未删除元素的子段后面,即:
dp0[i] = max(nums[i], dp0[i-1] + nums[i])
这其实就是经典的最大子段和的递推公式。 -
对于
dp1[i](已经删除过一个元素):
这里有两种可能:
a. 删除操作发生在当前元素之前,即当前元素nums[i]必须被保留,而前面的某个元素被删除了。那么,dp1[i]可以由dp1[i-1] + nums[i]得到(因为已经删除过,现在只能继续保留当前元素)。
b. 删除操作就是删除当前元素nums[i]本身。那么,以nums[i]结尾且删除了它,实际上相当于子段以nums[i-1]结尾且没有删除过,即dp0[i-1]。
综合两者:
dp1[i] = max(dp1[i-1] + nums[i], dp0[i-1])
注意:第二种情况中,dp0[i-1]可能为负,但即使为负,删除当前元素后子段和就是dp0[i-1](相当于子段只到i-1,i被删除,子段不能为空,所以至少保留i-1这个元素)。
-
-
初始条件
我们需要考虑第一个元素i=0的情况:dp0[0] = nums[0]:以第一个元素结尾且没删除,就是它自身。dp1[0] = 0?这里需要仔细思考。dp1[0]表示以第 0 个元素结尾且已经删除过一个元素。但如果我们删除了第 0 个元素,那么以第 0 个元素结尾的子段就不存在了(因为结尾元素被删了)。所以实际上,dp1[0]应该是一个无效状态,但在递推中,我们可以将它初始化为一个非常小的数(比如-inf),表示不可能。不过,更常见且简便的处理是:
我们允许删除操作后子段可以为空吗?题目要求子段是非空的,但“删除一个元素”可能使得剩余部分为空吗?如果数组只有一个元素,删除它后子段为空,这是不允许的。因此,在只有单个元素时,不能应用删除操作。
但为了递推方便,我们可以这样初始化:
dp1[0] = -inf或一个很小的数,这样在后续递推中,dp1[1]会正确地由dp0[0]得到(即删除第二个元素,保留第一个)。
在代码中,我们可以简单地将dp1[0]初始化为0吗?不可以,因为dp1[0]如果为 0,在递推dp1[1]时,dp1[0] + nums[1]就变成了nums[1],这表示“已经删除过元素且以第 1 个元素结尾”可以从“以第 0 个元素结尾且已删除”加上nums[1]得到,但dp1[0]本身是不合理的。所以稳妥起见,我们应设置dp1[0] = -inf。
不过,由于我们最终会取所有dp0[i]和dp1[i]的最大值,且dp1[0]无效,我们可以将dp1[0]初始化为一个非常小的数,比如-10^9以下,确保它不会被选为最大值。
-
递推计算
从i=1开始遍历数组,按上述方程计算dp0[i]和dp1[i]。
同时,我们维护一个全局最大值ans,每次更新ans = max(ans, dp0[i], dp1[i])。 -
边界情况
- 数组长度为 1 时:只能选择这个元素,不能删除(删除后为空),所以答案就是
nums[0]。 - 数组全为负数时:我们可以选择绝对值最小的负数,或者删除它?实际上,如果全为负数,最大子段和就是最大的那个负数(不删除),因为删除一个元素并不会让和变大(删除后可能得到更小的负数或空)。我们的递推仍然能正确处理。
- 数组长度为 1 时:只能选择这个元素,不能删除(删除后为空),所以答案就是
-
复杂度分析
时间复杂度:O(n),只需遍历一次数组。
空间复杂度:O(n),可以优化到 O(1) 只用几个变量。
示例推演
以 nums = [1, -2, 0, 3] 为例:
初始化:
dp0[0] = 1dp1[0] = -inf(表示不可能)ans = 1
i=1 (nums[1] = -2):
dp0[1] = max(-2, 1 + (-2)) = max(-2, -1) = -1dp1[1] = max(dp1[0] + (-2), dp0[0]) = max(-inf, 1) = 1(这里对应删除当前元素 -2,保留前面的 1)ans = max(1, -1, 1) = 1
i=2 (nums[2] = 0):
dp0[2] = max(0, -1 + 0) = max(0, -1) = 0dp1[2] = max(dp1[1] + 0, dp0[1]) = max(1 + 0, -1) = 1ans = max(1, 0, 1) = 1
i=3 (nums[3] = 3):
dp0[3] = max(3, 0 + 3) = 3dp1[3] = max(dp1[2] + 3, dp0[2]) = max(1 + 3, 0) = 4ans = max(1, 3, 4) = 4
最终答案为 4,与例子一致。
代码实现(Python)
def maximumSum(arr):
n = len(arr)
if n == 1:
return arr[0]
dp0 = [0] * n
dp1 = [0] * n
dp0[0] = arr[0]
dp1[0] = -10**9 # 表示不可能
ans = arr[0]
for i in range(1, n):
dp0[i] = max(arr[i], dp0[i-1] + arr[i])
dp1[i] = max(dp1[i-1] + arr[i], dp0[i-1])
ans = max(ans, dp0[i], dp1[i])
return ans
空间优化
由于 dp0[i] 和 dp1[i] 只依赖前一项,可以只用两个变量:
def maximumSum(arr):
n = len(arr)
if n == 1:
return arr[0]
dp0 = arr[0]
dp1 = -10**9
ans = arr[0]
for i in range(1, n):
new_dp0 = max(arr[i], dp0 + arr[i])
new_dp1 = max(dp1 + arr[i], dp0) # 注意这里 dp0 是旧的,即 dp0[i-1]
ans = max(ans, new_dp0, new_dp1)
dp0, dp1 = new_dp0, new_dp1
return ans
总结
这个问题的关键是将“允许删除一个元素”转化为两种状态:是否已经删除过。通过分别维护“未删除”和“已删除”的最大子段和,我们可以在线性时间内解决问题。此方法清晰且高效,是处理此类变种问题的典型思路。