LeetCode-53-最大子数组和

本文介绍两种高效算法解决最大子数组和问题:动态规划法与分治法。动态规划法通过定义状态转移方程简化子问题,实现O(n)时间复杂度。分治法则采用递归方式,通过合并子区间信息得出最优解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述

1、动态规划法

在使用动态规划法解决问题时,我们需要确保子问题之间是没有不确定的地方的,即需要确保子问题之间是无后效性的。无后效性实际上是这样的一个定义:

为了保证计算子问题能够按照顺序、不重复地进行,动态规划要求已经求解的子问题不受后续阶段的影响。这个条件也被叫做「无后效性」。换言之,动态规划对状态空间的遍历构成一张有向无环图,遍历就是该有向无环图的一个拓扑序。有向无环图中的节点对应问题中的「状态」,图中的边则对应状态之间的「转移」,转移的选取就是动态规划中的「决策」。

在确定子问题时,最简单的想法是将dp数组定义为前i位中的最大连续子数组,这样子我们只需要遍历一遍数组,则最后的dp值就是我们需要求的最大子数组和。但这样子分割子问题存在如下问题:当i移动的时候,我们可能出现多种情况,可能最大连续子数组不变,可能需要加上最新的nums[i],可能只有最新的nums[i],也可能需要从原先的最大连续子数组中删除掉一些数,这样导致子问题之间的关系不够清晰。

为了克服上述缺点,我们可以将dp数组定义成以nums[i]作为结尾的连续子数组的最大和是多少。这样子做的好处有如下几点:1、子问题之间存在的关联较小,我们只需要比较dp[i-1]+nums[i]和nums[i]的值那一个更大即可,避免了对于过多关系的讨论;2、最终在求解最大和时,我们只需要再遍历一遍dp数组即可返回最大值,时间复杂度仍然是O(n)。

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        vector<int> dp(nums.size(), 0);
        int result = INT_MIN;
        dp[0] = nums[0];
        for (int i = 1; i < nums.size(); ++i) {
            dp[i] = max(dp[i - 1] + nums[i], nums[i]);
        }
        for (auto i: dp) {
            result = max(result, i);
        }
        return result;
    }
};

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

其中第二份代码在一次循环中完成了对dp的设置以及对dp的比较,进一步降低时间复杂度。

2、分治法

使用分治法解决问题时我们同样需要将问题分解成多个子问题,区别于动态规划法,分治法解决问题时子问题之间通常是递归的关系,我们通过求解至子问题而后回退至上一层,一步步求得初始结果。

我们可以定义get(a,l,r)表示查询序列a的[l,r]区间内的最大连续子数组的和,因而最终的目标是求解get(nums,o,nums.size()-1)。为了使用分治法,我们可以考虑将区间[l,r]分为两段,假设我们取m=(l+r)/2,则两段分别为[l,m],[m+1,r]。当每一段的长度为1时就是我们的子问题,而后我们需要将左右两段合并返回上一层的结果。其中最关键的在于:1、我们维护什么信息;2、我们怎么合并信息。

我们定义一个结构体内有四个变量:1、lSum 表示 [l,r]内以 l 为左端点的最大子段和;2、rSum 表示 [l,r] 内以 r 为右端点的最大子段和;3、mSum 表示 [l,r] 内的最大子段和;4、iSum 表示 [l,r] 的区间和。显然,当子段长度为1时,四个值都是相同的值,即nums[i]。当我们需要对子段进行合并时,我们需要考虑如下的方式进行合并:1、合并iSum,我们只需要将左右子区间的iSum相加即可;2、合并lSum,我们需要取原先左子区间的lSum和左子区间的iSum加上右子区间的lSum中的最大值;3、合并rSum,我们需要取原先右子区间的rSum和右子区间的iSum加上左子区间的rSum中的最大值;4、合并mSum,我们需要对两种情况进行讨论:一是该连续子数组没有跨越m,此时取左子区间的mSum 和右子区间的mSum 的较大值;二是该连续子数组跨越m,此时取左子区间的rSum 和右子区间的lSum 之和,三者取最大值即可。

class Solution {
public:
    struct Status {
        int lSum, rSum, mSum, iSum;
    };

    Status pushUp(Status l, Status r) {
        int iSum = l.iSum + r.iSum;
        int lSum = max(l.lSum, l.iSum + r.lSum);
        int rSum = max(r.rSum, r.iSum + l.rSum);
        int mSum = max(max(l.mSum, r.mSum), l.rSum + r.lSum);
        return (Status) {lSum, rSum, mSum, iSum};
    };

    Status get(vector<int> &a, int l, int r) {
        if (l == r) {
            return (Status) {a[l], a[l], a[l], a[l]};
        }
        int m = (l + r) / 2;
        Status lSub = get(a, l, m);
        Status rSub = get(a, m + 1, r);
        return pushUp(lSub, rSub);
    }

    int maxSubArray(vector<int>& nums) {
        return get(nums, 0, nums.size() - 1).mSum;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值