动态规划-最大子数组和
最大子数组和
题目描述
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。
53. 最大子数组和 - 力扣(LeetCode)
示例 :
输入:nums = [-2,1,-3,4,-1,2,1,-5,4] 输出:6 解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
解题思路
这个问题是动态规划的一个经典问题,也被称作“最大子序和问题”或“最大子数组和”。解决这个问题的思路是遍历数组,在遍历的过程中,我们维护一个当前遍历到的子数组的最大和,以及全局的最大和。每当我们遍历到一个新的元素时,我们考虑两种情况:
- 如果将当前元素加入之前的子数组会使得子数组的和变得更大,那么我们就更新当前子数组的和。
- 如果当前元素自己就是一个更大的子数组(即之前的子数组和小于0,加入当前元素还不如只包含当前元素),那么我们重新开始一个新的子数组,其和就是当前元素的值。
动态规划思路
在最大子序和问题中,我们想要找到一个具有最大和的连续子数组。这个问题可以分解为多个子问题:对于数组中的每个位置,我们需要知道以该位置结尾的最大子数组和是多少。
定义状态
我们定义一个数组dp
,其中dp[i]
表示以nums[i]
结尾的连续子数组的最大和。注意,这里的定义稍微有些不同,因为我们实际上并不需要显式地存储整个dp
数组(尽管在某些情况下这样做可以方便调试和理解),而是可以通过变量来维护当前的最大和(即dp
数组中的最后一个有效值)以及全局的最大和。
状态转移方程
对于每个位置i
(从1开始计数,假设dp[0] = nums[0]
),我们需要根据前一个状态dp[i-1]
和当前元素nums[i]
来更新dp[i]
。这里有两种情况:
- 如果
dp[i-1] + nums[i] > nums[i]
,即加上当前元素后子数组的和变得更大,那么我们就应该继续扩展这个子数组,即dp[i] = dp[i-1] + nums[i]
。 - 否则,如果
dp[i-1] + nums[i] <= nums[i]
,即加上当前元素后子数组的和没有变得更大(甚至可能变得更小),那么我们就应该重新开始一个新的子数组,只包含当前元素,即dp[i] = nums[i]
。
但是,在实际编程中,我们并不需要显式地创建一个dp
数组来存储每个位置的状态,因为我们可以只通过几个变量来维护当前的最大和以及全局的最大和。
初始化
- 全局最大和初始化为数组的第一个元素(或者一个足够小的数,但在这个问题中,由于子数组至少包含一个元素,所以初始化为第一个元素是合理的)。
- 当前最大和也初始化为数组的第一个元素。
遍历数组
从数组的第二个元素开始遍历,对于每个元素,根据状态转移方程更新当前最大和,并检查是否需要更新全局最大和。
返回结果
遍历结束后,全局最大和就是我们要找的最大子数组和。
代码示例
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n = nums.size();
vector<int> dp(n, 0);
dp[0] = nums[0];
for (int i = 1; i < n; i++) {
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
}
int ret = dp[0];
for (int i = 1; i < n; i++) {
ret = max(ret, dp[i]);
}
return ret;
}
};
环形子数组的最大和
题目描述
给定一个长度为 n 的环形整数数组 nums ,返回 nums 的非空 子数组 的最大可能和 。 环形数组 意味着数组的末端将会与开头相连呈环状。形式上, nums[i] 的下一个元素是 nums[(i + 1) % n] , nums[i] 的前一个元素是 nums[(i - 1 + n) % n] 。 子数组 最多只能包含固定缓冲区 nums 中的每个元素一次。形式上,对于子数组 nums[i], nums[i + 1], ..., nums[j] ,不存在 i <= k1, k2 <= j 其中 k1 % n == k2 % n 。
918. 环形子数组的最大和 - 力扣(LeetCode)
解题思路
为了解决这个问题,我们可以将问题分解为两个子问题:
-
非环形数组的最大子数组和:这是经典的Kadane算法问题,其中我们需要找到非环形数组中的最大子数组和。
-
环形数组中跨越首尾的最大子数组和:这可以通过计算原数组中去掉某个最小子数组后的最大剩余和来找到。换句话说,我们找到数组中的一个最小子数组(可能是连续的),然后从数组中去掉这个子数组,剩下的部分就是我们要找的可能跨越首尾的最大子数组。
核心思路分为两部分:
- 计算非环形情况下的最大子数组和:
- 使用
f
数组来记录以每个位置结尾的子数组的最大和。对于f[i]
,它的值等于max(f[i-1] + nums[i], nums[i])
,即要么是当前元素自身,要么是前一个子数组的和加上当前元素(如果加上当前元素能使和更大)。 - 遍历整个数组,同时更新
big
变量,记录到当前位置为止的最大子数组和。
- 使用
- 计算环形情况下可能跨越首尾的最大子数组和:
- 考虑到环形特性,一个可能的最大子数组可能跨越数组的首尾。为了找到这种子数组,我们需要知道如果排除某个最小子数组后,剩余部分的和是否可能更大。
- 使用
g
数组来记录以每个位置结尾的子数组的最小和(这里的“最小和”是为了后续计算方便,实际是为了找到可以排除的最小部分)。对于g[i]
,它的值等于min(g[i-1] + nums[i], nums[i])
,即要么是当前元素自身,要么是前一个子数组的和加上当前元素(即使加上当前元素使和更小,因为我们要找的是最小和)。 - 遍历整个数组,同时更新
sma
变量,记录到当前位置为止的最小子数组和。 - 累加整个数组的元素到
sum
变量中,这个变量代表了整个数组的和。 - 最后,比较两种情况:
- 如果整个数组的和
sum
等于最小子数组和sma
,说明数组中所有元素都是负数,或者所有正数都已经被最小子数组所包含。此时,最大子数组和只能是big
(即非环形情况下的最大子数组和)。 - 否则,可能的最大子数组和要么是
big
(非环形情况),要么是sum - sma
(排除最小子数组后的剩余部分)。取两者中的较大值作为结果。
- 如果整个数组的和
代码示例
class Solution {
public:
int maxSubarraySumCircular(vector<int>& nums) {
int n = nums.size();
vector<int> f(n, INT_MIN); // 最大
vector<int> g(n, INT_MAX); // 最小
f[0] = g[0] = nums[0];
for (int i = 1; i < n; i++) {
f[i] = max(f[i - 1] + nums[i], nums[i]);
g[i] = min(g[i - 1] + nums[i], nums[i]);
}
int big = f[0];
int sma = g[0];
int sum = nums[0];
for (int i = 1; i < n; i++) {
big = max(big, f[i]);
sma = min(sma, g[i]);
sum += nums[i];
}
if (sum == sma)
return big;
else
return max(big, sum - sma);
}
};