[Edp] lc53. 最大子序和(dp+分治+算法优化+详细分析)

1. 题目来源

链接:53. 最大子序和

进阶力扣,字节常见的两题组合拳:面试题 17.24. 最大子矩阵

进阶题:

官方非常好的题解:连续子数组的最大和

2. 题目解析

这题真的是绝了!

显然,暴力是 O ( n 2 ) O(n^2) O(n2) 的。dp 能做到 O ( n ) O(n) O(n) 时间, O ( 1 ) O(1) O(1) 空间。一般的分治时间是 O ( n l o g n ) O(nlogn) O(nlogn),空间 O ( l o g n ) O(logn) O(logn)。本文参考题解给出的分治思想,能做到 O ( n ) O(n) O(n) 的时间, O ( l o g n ) O(logn) O(logn) 的空间,真是绝了!

变量写法远比数组简单,数组这边界情况真的麻烦…关键还是写的太烂了第一次…本文
在这里插入图片描述


补一个 O ( n l o g n ) O(nlogn) O(nlogn) 的分治。

思路:

  • 区间 [l, r] 内的答案可以分为三部分来计算,记 mid = l+r >> 1
    • 区间 [l, mid] 内的答案(递归计算)。
    • 区间 [mid + 1, r] 内的答案(递归计算)。
    • 跨越 midmid + 1 的连续子序列。
  • 其中,第 (3) 部分只需要从 mid 开始向 l 找连续的最大值,以及从 mid+1 开始向 r 找最大值即可,在线性时间内可以完成。
  • 递归终止条件显然是 l == r ,此时直接返回 nums[l]

  • 时间复杂度:由递归主定理 S ( n ) = 2 S ( n / 2 ) + O ( n ) S(n)=2S(n/2)+O(n) S(n)=2S(n/2)+O(n),解出总时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。或者每一层时间复杂度是 O ( n ) O(n) O(n),总共有 O ( l o g n ) O(logn) O(logn) 层,故总时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)
  • 空间复杂度 O ( l o g n ) O(logn) O(logn)

代码:

// 麻烦的数组写法
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        if (nums.size() == 1) return nums[0];

        vector<int> f(nums.size(), INT_MIN);
        f[0] = nums[0];
        int res = max(f[0], INT_MIN);
        for (int i = 1; i < nums.size(); i ++ ) {
            f[i] = max(nums[i], f[i - 1] + nums[i]);
            res = max(res, f[i]);
        }
        return res;
    }
};

// 简化
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        vector<int> f(nums.size());
        f[0] = nums[0];
        int res = f[0];
        for (int i = 1; i < nums.size(); i ++ ) {
            f[i] = max(nums[i], nums[i] + f[i - 1]);
            res = max(res, f[i]);
        }
        return res;
    }
};

// 再简化代码
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        vector<int> f(nums.size() + 1);
        int res = INT_MIN;
        for (int i = 0; i < nums.size(); i ++ ) {
            f[i + 1] = max(nums[i], f[i] + nums[i]);
            res = max(res, f[i + 1]);
        }
        return res;
    }
};

// 滚动数组优化
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        if (nums.size() == 1) return nums[0];

        vector<int> f(2, INT_MIN);
        f[0] = nums[0];
        int res = max(f[0], INT_MIN);
        for (int i = 1; i < nums.size(); i ++ ) {
            f[i&1] = max(nums[i], f[i - 1&1] + nums[i]);
            res = max(res, f[i&1]);
        }
        return res;
    }
};

// 变量写法
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int res = INT_MIN;
        for (int i = 0, last = 0; i < nums.size(); i ++ ) {
            last = nums[i] + max(0, last);
            res = max(res, last);
        }
        return res;
    }
};

// 分治写法  O(n)
class Solution {
public:

    struct Node {
        int sum, s, ls, rs;     // 总和,最大子段和,最大前缀,最大后缀
    };

    Node build(vector<int> &nums, int l, int r) {
        if (l == r) {
            // 若数组全为负数则t为0,若数组有一个非负数,则结果就为正
            int t = max(nums[l], 0);  // 允许区间为空的处理方法。最后再特判所有负数情况即可
            return {nums[l], t, t, t};
        }

        int mid = l + r >> 1;
        auto L = build(nums, l, mid), R = build(nums, mid + 1, r);

        Node res;                           // 左右区间合并更新数据
        res.sum = L.sum + R.sum;            // 左右区间总和
        res.s = max(max(L.s, R.s), L.rs + R.ls);    // 左右区间最大子段和,左区间最大后缀,右区间最大前缀
        res.ls = max(L.ls, L.sum + R.ls);   // 左区间最大后缀,左区间最大后缀和左区间总和加右区间最大前缀
        res.rs = max(R.rs, R.sum + L.rs);   // 右区间最大前缀,右区间最大前缀和右区间总和加左区间最大后缀
        return res;
    }

    int maxSubArray(vector<int>& nums) {
        int res = INT_MIN;
        for (auto e : nums) res = max(res, e);      // 判断所有数字均为负数的情况
        if (res < 0) return res;
        
        auto t = build(nums, 0, nums.size() - 1);
        
        return t.s;
    }
};


// 补充的分治  O(nlogn)
class Solution {
public:
    int calc(int l, int r, vector<int>& nums) {
        if (l == r)
            return nums[l];
        int mid = (l + r) >> 1;
        int lmax = nums[mid], lsum = 0, rmax = nums[mid + 1], rsum = 0;

        for (int i = mid; i >= l; i--) {
            lsum += nums[i];
            lmax = max(lmax, lsum);
        }

        for (int i = mid + 1; i <= r; i++) {
            rsum += nums[i];
            rmax = max(rmax, rsum);
        }

        return max(max(calc(l, mid, nums), calc(mid + 1, r, nums)), lmax + rmax);
    }

    int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        return calc(0, n - 1, nums);
    }
};

20210421 更新,要处理最大子矩阵问题…回过头来看 1 维简化版本。博文写的好乱…

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        vector<int> f(n, -1e9);

        f[0] = nums[0];
        for (int i = 1; i < n; i ++ ) f[i] = max(f[i - 1] + nums[i], nums[i]);

        return *max_element(f.begin(), f.end());
    }
};

20210421 又更新。

有个比较好的思路,可以预处理前缀和,枚举终点前缀合 i,维护一个左半段区间的最小前缀和,再用其减去前半段的最小前缀和即为一段连续的区间,也是以 i 结尾的最大子序和,每次更新这个左半段区间的最小前缀和即可。也是一种很形象的思路。

时间复杂度: O ( n ) O(n) O(n)
空间复杂度: O ( n ) O(n) O(n)

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        vector<int> f(n + 1);
        for (int i = 1; i <= n; i ++ ) f[i] = f[i - 1] + nums[i - 1];
        int ans = -1e9, mn = 0;
        for (int i = 1; i <= n; i ++ ) ans = max(ans, f[i] - mn), mn = min(mn, f[i]);
        return ans;
    }
};

20210717 再更新。

每日一题,状态转移为 f[i]=max(f[i-1]+nums[i], nums[i]); 可发现 f[i] 仅有上一层状态有关,故可以利用滚动数组的优化,用单变量代替 f 数组。

注意写法:

  • 不要让 f=nums[0],否则在单个元素下就直接返回 res 了。
  • i 从 0 开始,上一层的 f 进行状态转移,然后对本层 f 进行更新。非常精巧的写为 f = max(f + nums[i], nums[i]);
  • 仍需要记录以 i 结尾的最大连续子数组和,而不是直接返回 f,这里的 ff[n-1] 是一样的。每次都会更新,丢到上次的值。所以需要记录状态转移过程中最大的 f
  • 由于本题的代码特殊性,再抽象为 f = max(f, 0) + nums[i];f 是否非负,非负的话,这一段一定不需要加入到答案中,但是具体的 dp 过程的思考是从 f = max(f + nums[i], nums[i]); 过度过来的。
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        int f = 0;
        int res = -1e9;
        for (int i = 0; i < n; i ++ ) {
            f = max(f + nums[i], nums[i]);  
            res = max(f, res);
        }

        return res;
    }
};
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值