连续子数组的最大和
问题1:给定任意一个数组A求最大的连续子数组的和
动态规划思想
描述:dp[i]表示以A[i]为尾元素的最大连续子数组
状态转移方程:dp[i + 1] = max(dp[i], 0) + A[i + 1]
初始化dp[0]为 0
证明
假设存在一个以A[i]为尾元素的连续子数组的和为m,使得m + A[i+1] > dp[i] + A[i+1],则m > dp[i],这与我们的假设前提矛盾,,所以m <= dp[i]。即以A[i + 1]为尾元素的连续子数组的长度不为1时,最大和dp[i] + A[i]。又考虑到连续子数组长度为1的情况,得出状态转移方程。
实现
int maxSubSum(const vector<int> & nums) {
int n = nums.size();
vector<int> dp(n + 1); // 多填充一个处理边界条件
for (int i = 0; i < n; i++)
dp[i + 1] = max(dp[i], 0) + nums[i];
return dp.back();
}
通过上面的代码分析,可以发现dp[i + 1]只用到dp[i],不需要用到其他记录值,见缝插针空间优化
int maxSubSum(const vector<int> & nums) {
int ans = 0, dp = 0;
for (auto i : nums)
ans = max(ans, dp = max(dp, 0) + i);
return ans;
}
变形
问题2:在问题1的基础上加一个限制,假定选中的子数组的交替加减求和,即对每个子数组的奇数序元素使用加法,偶数序元素使用减法,使用元素的例如对于数组{1, 2, 3, 4, 5}的子数组{2, 3, 4} 的和为2 - 3 + 4 = 3。
思路
首先还是子数组的最大和问题,第一反应是先交错取相反数求和再对整个数组取相反数求和,这样就没必要在每个位置进行加减了。但是这种方法对于很多情况是难以预测的,比如最优解子数组的上一个元素刚好是负数求出来的解必定是错的,还有其他更复杂的情况。
透过现象看本质,变形的问题就是我们没办法确定在每一位取最优解的起始元素是取的相反数还是本身。既然没法确定那就规定。规定dp[i]为A[i]为首元素子数组最大和,那这个dp[i]怎么来呢?既然已A[i]为首元素那么在子数组中的第一个元素必是其原来的值,下一个数取相反数(如果有或者说如果需要)。但是dp[i+1]保存的只是A[i+1]取其原值的最优解,因此还需要保存以A[i+1]相反数为首元素的最优解。与问题1的证明同理,这个思路的可行性肉眼可见。
描述:dp0[i]为A[i]为首元素的子数组的最大和,dp1[i]为A[i]相反数为首元素的子数组的最大和
状态转移方程:
dp0[i] = max(dp1[i], 0) + A[i]
dp1[i] = max(dp0[i], 0) - A[i]
直接空间优化,逆序求解,保存每一个步dp0取最优解, go语言max自行实现
// 最大的连续子数组
func maxSubSum(nums []int) int {
var dp0, dp1, ans int
for i := len(arr) - 1; i >= 0; i-- {
t0 := max(dp1, 0) + nums[i]
t1 := max(dp0, 0) - nums[i]
dp0, dp1 = t0, t1
ans = max(ans, dp0)
}
return ans
}