Bootstrap

【最大子数组和】问题具体思路——【循环暴搜】【贪心】【动态规划】【分治】(附完整代码)


前言

本文将围绕【最大子数组和】问题展开讨论。

对于这一问题将采用多种思路方法来解决

【循环暴搜】【贪心】【动态规划】【分治】


一、如何理解【最大子数组和】?

题目链接: [点击跳转] Leetcode 53. 最大子数组和

题目如下:

请添加图片描述
这是一道经典的数组题,可以尝试多种方法来解决。


二、方法一(循环暴搜)

(方法一时间复杂度高易超时,也可从方法二开始浏览)

在读过题之后,我们很容易就会想到:

既然是找子数组,那么我们就可以做一个嵌套循环,一个双层的循环就可以表示任意一个含两个元素的子数组了。

就像这样:

for(int i=0;i<size;i++){
    for(int j=i+1;j<size;j++){

    }
}//(一个指向子数组开头,一个指向子数组末尾)

在这个嵌套循环进行的过程中,将元素累加起来,并记录下最大的子数组。

最后在找一遍单个元素的子数组,取最大,就OK啦!

代码如下:

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int sum=0;//子数组和
        int size=nums.size();//数组大小
        int maxsum=INT_MIN;//表示最大子数组

        //判断两个及以上的相邻元素
        for(int i=0;i<size;i++){
            sum+=nums[i];
            for(int j=i+1;j<size;j++){
                sum+=nums[j];
                maxsum=max(sum,maxsum);
            }
            //sum清0
            sum=0;
        }

        //单个元素
        for(int k=0;k<size;k++){
            maxsum=max(maxsum,nums[k]);
        }

        return maxsum;
    }
};

!!!!但是,如果所给数组很长,由于我们使用嵌套循环,时间复杂度为O(n^2),我们很有可能会导致超时。


三、方法二(贪心)

最大子数组有一个特点,这个数组的开头和结尾都不会有负数(因为如果是负数的话,去掉这个负数之后,一定比原来的大,那它就不是最大子数组了)

因此我们可以延申一下,最大子数组的开头和结尾都不会有 和为负数的子数组(因为如果子数组和是负数的话,去掉这个子数组之后,一定比原来的大,那它就不是最大子数组了)

所以我们只需要走一遍循环,在循环中只有子数组和不为负数,才继续往下走,否则就清空,以下一个元素为起始,继续下去,直到走完整个数组,返回最大的子数组即可。

代码如下:

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int sum=0;//记录子数组
        int maxsum=INT_MIN;//最大子数组
        for(int i=0;i<nums.size();i++){
            sum+=nums[i];
            maxsum=max(maxsum,sum);
            if(sum<=0){//跳出
                sum=0;//清0
            }
        }
        return maxsum;
    }
};

时间复杂度为O(n)。


四、方法三(动态规划)

思路类似于贪心,首先动态规划先将问题分解成子问题。

比如说,给定的数组长度为n。
我们记录下以第0位元素结尾时的最大子数组。
我们再记录下以第1位元素结尾时的最大子数组。
我们再记录下以第2位元素结尾时的最大子数组。
我们再记录下以第3位元素结尾时的最大子数组。
······
我们再记录下以第n位元素结尾时的最大子数组。

我们发现以
当第0位元素结尾时的最大子数组>0时
第1位元素结尾时的最大子数组=第0位元素结尾时的最大子数组+第1位元素

当第0位元素结尾时的最大子数组<=0时
第1位元素结尾时的最大子数组=第1位元素

因此我们只需在一层循环中,找到以第?位元素结尾时子数组最大就可以啦!

template<typename T>
void set_max(T &a,T &b) {
    a = max(a, b);
}
class Solution {
private:
public:
    int maxSubArray(vector<int> &nums) {
        int n = (int) nums.size();
        vector<int> dp(n, 0);
        dp[0] = nums[0];
        int res = nums[0];
        for (int i = 1; i < n; ++i) {
            set_max(res, dp[i] = max(dp[i - 1] + nums[i], nums[i]));
        }
        return res;
    }
};

时间复杂度为O(n)。


五、方法四(分治)

分治算法的核心是将一个大问题分解为多个较小规模的子问题,分别求解这些子问题,然后将子问题的解合并得到原问题的解。

采用分治算法将数组划分为左右两个子数组,然后分别求解左子数组、右子数组以及跨越中间位置的子数组的最大子数组和,最后取三者中的最大值作为原问题的解。

由于子问题具有相似的结构,可以使用递归来求解子问题。对于最大子数组和问题,递归地求解左子数组和右子数组的最大子数组和,然后再处理跨越中间位置的子数组,最后将三个部分的最大子数组和进行比较,得到原问题的解。

代码如下:

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        return maxSubArrayHelper(nums, 0, nums.size() - 1);
    }

    int maxSubArrayHelper(vector<int>& nums, int left, int right) {
        // 说明子数组只有一个元素
        if (left == right) return nums[left];
        int mid = left + (right - left) / 2;
        // 递归计算左子数组的最大子数组和
        int leftMax = maxSubArrayHelper(nums, left, mid);
        // 递归计算右子数组
        int rightMax = maxSubArrayHelper(nums, mid + 1, right);
        // 计算跨越中间位置的子数组的最大子数组和
        int crossMax = findCrossMax(nums, left, mid, right);
        // 返回最大值
        return max({leftMax, rightMax, crossMax});
    }
    int findCrossMax(vector<int>& nums, int left, int mid, int right) {
        int leftSum = INT_MIN, rightSum = INT_MIN, sum = 0;
        // 计算左边部分的最大子数组和
        for (int i = mid; i >= left; --i) {
            sum += nums[i];
            leftSum = max(leftSum, sum);
        }
        sum = 0;
        // 计算右边
        for (int i = mid + 1; i <= right; ++i) {
            sum += nums[i];
            rightSum = max(rightSum, sum);
        }
        // 返回左右总和
        return leftSum + rightSum;
    }
};

时间复杂度为O(nlogn)。


;