53. 最大子数组和
方法一、贪心算法
思路
假设,从第一个元素开始向后遍历累加,得[当前连续子数组和] sum。
当 sum 加上当前元素后,得到新 sum;
- 若新 sum 大于[最大连续的子数组和] maxSum 时,则更新 maxSum,说明当前遍元素是正数,对 sum 是增益效果;
- 若新 sum 没有大于 maxSum 时,此时按兵不动,说明当前元素是一个相反数小于 sum 的负数,这个负数对 sum 是减益效果,sum 加上当前元素后变小,但不影响大局;
- 若新 sum 小于 0 时,说明当前元素是一个相反数大于 sum 的负数,sum 加上当前元素后,全盘皆输。sum 要放弃当前连续序列,从下一元素重新开始累加。
复杂度
时间复杂度:O(n)
空间复杂度:O(1)
代码
class Solution {
/**
* @param Integer[] $nums
* @return Integer
*/
function maxSubArray($nums) {
// 最大连续的子数组和
$maxSum = PHP_INT_MIN;
// 当前连续子数组和
$sum = 0;
for($i = 0; $i < count($nums); $i++) {
$sum = $sum + $nums[$i];
// 若超过最大连续的子数组和,则更新
if($sum > $maxSum) {
$maxSum = $sum;
}
// 当前连续和小于0,则需重置,重新连续计算
if($sum <= 0) {
$sum = 0;
}
}
return $maxSum;
}
}
方法二、动态规划法
思路
题外话:@老虎 老哥在详细解读动态规划的实现, 易理解一文中提到,动态规划的核心是“以子序列的结束点为基准,向前递推子序列关系”这点,个人感觉对理解动态规划有很大的帮助。
将求 [整个序列最大序列和] 拆分成求 [以a[i]为子序列末端的最大序子列连续和],将 [以a[i]为子序列末端的最大序子列连续和],记为 f(i)。
若求 [整个序列最大序列和] 则是求 i 取 [0, len -1]时,f(i) 的最大值。
如何求 f(i)?
f(i) 与 f(i-1) 、a[i] 有关。[以a[i-1]为子序列末端的最大序子列连续和],即 f(i-1) 加上当前元素 a[i] 后,
- 若“和”增加,f(i-1) 对 a[i]是增益效果,则 f(i-1) + a[i] 作为 f(i) 的值;
- 若“和”减少,f(i-1) 对 a[i]是减益效果,则抛弃 f(i-1),直接取 a[i] 作为 f(i)。(取a[i]相当于重新开始去探索可能存在的新的连续序列最大值)
所以可以得到动态转移方程:
f(i) = max( f(i-1) + a[i], a[i] )
复杂度
时间复杂度:O(n)
空间复杂度:O(1)
代码
class Solution {
/**
* @param Integer[] $nums
* @return Integer
*/
function maxSubArray($nums) {
// 最大和
$max = PHP_INT_MIN;
// 记录以a[i]为子序列末端的最大序子列连续和
$sum = 0;
for($i = 0; $i < count($nums); $i++) {
$sum = max($sum + $nums[$i], $nums[$i]);
$max = max($sum, $max);
}
return $max;
}
}
变形题
152. 乘积最大子数组
思路
基于53. 最大子数组和的变形;
[152. 乘积最大子数组]相比[53. 最大子数组和],要注意的是“乘法”下由于两个负数的乘积也依然可能得到一个很大的正数,所以必须同时计算“最小子数组和”。(前面结果是负的,当前元素是负的,两者相乘有可能翻盘,成为最大子数组)。
- fmax[i] 表示以 i 结尾的最大子数组乘积;
- fmin[i] 表示以 i 结尾的最小子数组乘积;
状态转移方程:
fmax[i] = max{fmax[i-1] * nums[i], fmin[i-1] * nums[i], nums[i]}
fmin[i] = min{fmax[i-1] * nums[i], fmin[i-1] * nums[i], nums[i]}
递推结束条件:
fmax[0] = nums[0]
fmin[0] = nums[0]
复杂度
时间复杂度:O(n);
空间复杂度:O(n) ,可优化至 O(1);
代码
func maxProduct(nums []int) int {
fmax := make([]int, len(nums))
fmin := make([]int, len(nums))
fmax[0] = nums[0]
fmin[0] = nums[0]
for i := 1; i < len(nums); i++ {
fmax[i] = max(
max(fmax[i-1] * nums[i], fmin[i-1] * nums[i]),
nums[i],
)
fmin[i] = min(
min(fmax[i-1] * nums[i], fmin[i-1] * nums[i]),
nums[i],
)
}
maxAns := math.MinInt32
for _, v := range fmax {
maxAns = max(maxAns, v)
}
return maxAns
}
由于递推只与当前元素和f[i-1]有关,所以可以把hash优化成双指针,把空间复杂度将为O(1);
func maxProduct(nums []int) int {
fmaxP1, fmaxP2 := nums[0], nums[0]
fminP1, fminP2 := nums[0], nums[0]
maxAns := nums[0]
for i := 1; i < len(nums); i++ {
fmaxP2 = max(
max(fmaxP1 * nums[i], fminP1 * nums[i]),
nums[i],
)
fminP2 = min(
min(fmaxP1 * nums[i],fminP1 * nums[i]),
nums[i],
)
fmaxP1 = fmaxP2
fminP1 = fminP2
maxAns = max(maxAns, fmaxP2)
}
return maxAns
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func max(a, b int) int {
if a > b {
return a
}
return b
}