给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
一、动态规划
动态规划的难点就在于找到对应的算法表达式,需要积累经验。
若输入数组的长度是n,那么我们就是要找出区间在[0,n)内的子数组[i,j]最大和。如何抛弃掉数组前端的[0,i)这些使得数组变小的值呢?那就是将前面的数按照次序依次计算是否大于0。因此从下标为0的数组开始查找直到最后一个n-1,即:
若前面i-1个值总和大于0,那么意味着可以继续添加,不用判断添加的值是否是正值,因为i-1的值已经被保存在了dp[i-1]中;若dp[i-1]已经为负,那么继续添加值已经没有意义,重新开始计算即可。因此,整个表达式可以简化为:
代码:
int maxSubArray(vector<int>& nums) {
const int n = nums.size();
vector<int> dp(n);
int res = nums[0];
dp[0] = nums[0];
for(int i=1; i<n; ++i)
{
dp[i]=std::max(dp[i-1]+nums[i], nums[i]);
res = std::max(dp[i], res);
}
return res;
}
代码优化:
int maxSubArray(vector<int>& nums) {
int pre = 0, maxAns = nums[0];
for (const auto &x: nums) {
pre = max(pre + x, x);
maxAns = max(maxAns, pre);
}
return maxAns;
}
二、贪心算法
代码(同样可以进行优化):
int maxSubArray(vector<int>& nums)
{
const int n = nums.size();
int res = INT_MIN, sum = 0;
for (int i = 0; i < n; ++i)
{
sum += nums[i];
res = std::max(sum, res);
sum = sum < 0 ? 0 : sum;
}
return res;
}
三、暴力法
暴力法思路非常简单,从每一个元素开始查找它连续序列的最大和,再将这几个数进行比较得到最大的那个数。
代码:
int maxSubArray(vector<int>& nums)
{
const int n = nums.size();
int max = INT_MIN;
for (int i = 0; i < n; ++i)
{
int sum = 0;
for (int j = i; j < n; ++j)
{
sum += nums[j];
max = std::max(sum, max);
}
}
return max;
}
四、分治法
分治法的思路就是判断最大子序和出现的位置来进行处理:
1、子序在数组左边;
2、子序在数组右边;
3、子序在数组中间;
对于子序在数组左右两边情况就是取二者的最大值即可。但当子序在中间的时候需要通过类似于贪心法的方式求得在左右两边的最大值,再返回最后的值,最后与左右的值进行比较返回最大值,如此进行递归。
代码:
int maxSubArray(vector<int>& nums)
{
return maxSubArrayHelper(0, nums.size() - 1, nums);
}
int maxSubArrayHelper(int left, int right, vector<int>& nums)
{
if (left == right) return nums[left];
int mid = (left + right) >> 1;
int leftSum = maxSubArrayHelper(left, mid, nums);
int rightSum = maxSubArrayHelper(mid + 1, right, nums);
int crossSum = maxSubCrossHelper(left, mid, right, nums);
int res = std::max(leftSum, rightSum);
res = std::max(res, crossSum);
return res;
}
int maxSubCrossHelper(int left, int mid, int right, vector<int>& nums)
{
int leftSum = nums[mid], rightSum = nums[mid], sum = 0;
for (int i = mid; i >= left; --i)
{
sum += nums[i];
leftSum = std::max(sum, leftSum);
/* 这里不能如此处理,因为后续可能会有正数,并不能像贪心法那样重新计算总和 */
//if (sum < 0)
//{
// sum = 0;
// break;
//}
}
sum = 0;
for (int i = mid + 1; i <= right; ++i)
{
sum += nums[i];
rightSum = std::max(sum, rightSum);
//if (sum < 0)
//{
// sum = 0;
// break;
//}
}
return leftSum + rightSum;
}
优化后(使用宏定义的方式):
/* 需要了解是否有方法替换宏定义的方式 */
#define MAX_CROSS_ONESIDE(res, start, end, nums, compare, op) \
do { \
int sum = 0; \
for(int i = (start); (i) compare (end); op(i)) \
{ \
sum += nums[i]; \
*res = std::max(*res, sum); \
} \
} while(0) \
int maxSubCrossHelper(int left, int mid, int right, vector<int>&nums)
{
int leftSum = INT_MIN, rightSum = INT_MIN;
int* pLeft = &leftSum;
int* pRight = &rightSum;
MAX_CROSS_ONESIDE(pLeft, mid, left, nums, >=, --);
MAX_CROSS_ONESIDE(pRight, mid + 1, right, nums, <=, ++);
return leftSum + rightSum;
}
使用C++11的std::function特性:
int maxCrossOneSide(int start, int end, vector<int>&nums,
std::function<int(int, int)> op, std::function<int(int, int)> cmp)
{
int sum = 0, res = INT_MIN;
for (int i = start; cmp(i, end); i = op(i, 1))
{
sum += nums[i];
res = std::max(sum, res);
}
return res;
}
int maxSubCrossHelper(int left, int mid, int right, vector<int>&nums)
{
int leftSum = maxCrossOneSide(mid, left, nums, std::minus<int>(), std::greater_equal<int>());
int rightSum = maxCrossOneSide(mid + 1, right, nums, std::plus<int>(), std::less_equal<int>());
return leftSum + rightSum;
}