动态规划:8行代码搞定最大子数组和问题

👇👇关注后回复 “进群” ,拉你进程序员交流群👇👇

作者丨小风哥

来源丨码农的荒岛求生(ID:escape-it)

今天给大家带来一道极其经典的题目,叫做最大和子数组,给定一个数组,找到其中的一个连续子数组,其和最大。

示例:

输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 子数组[4,-1,2,1]的和为6,其它任何连续的子数组和不超过6.

想一想该怎样解决这个问题。

如果你一时想不到解法可以从暴利解法开始。

暴力求解

这种解法最简单,我们把所有子数组找出来,然后依次计算其和,找出一个最大的出来,比如给定数组[1,2,3],那么我们能找出子数组:[1],[2],[3],[1,2],[2,3],[1,2,3],很显然这里和最大的子数组为[1,2,3],其值为6。

int sum(vector<int>&nums, int b,int e){
    int res = 0;
    for (; b <= e; b++) {
        res += nums[b];
    }
    return res;
}
int maxSubArray(vector<int>& nums) {
    int size = nums.size();
    int res = 0x80000000;
    for (int i = 0; i < size; i++) {
        for (int j = i; j < size; j++) {
            res = max(res, sum(nums, i, j));
        }
    }
    return res;
}

这种解法最简单,该算法的时间复杂度为O(n^3),其中找出所有子数组的时间复杂度为O(n^2),计算每个子数组的和的时间复杂度为O(n),因此其时间复杂度为O(n^3)。

让我们再来看一下这个过程,这里的问题在于计算每个子数组的和时有很多重复计算,比如我们知道了子数组[1,2]的和后再计算数组[1,2,3]的值时完全可以利用子数组[1,2]的计算结果而无需从头到尾再算一遍,也就是说我们可以利用上一步的计算结果,这本身就是动态规划的思想。

80ee6e20a895d2f28b07948d539d0214.png

基于该思想我们可以对上述代码简单改造一下:

int maxSubArray(vector<int>& nums) {
    int size = nums.size();
    int res = 0x80000000;
    for (int i = 0; i < size; i++) {
        int sumer = nums[i];
        res = max(res, sumer);
        for (int j = i + 1; j < size; j++) {
            sumer += nums[j];
            res = max(res, sumer);
        }
    }
    return res;
}

看到了吧,代码不但更简洁,而且运行速度更快,该算法的时间复杂度为O(n^2),比第一种解法高了很多。

还有没有进一步提高的空间呢?

答案是肯定的。

分而治之

我们在之前的文章中说过,分治也是一种非常强大的思想,具体应该这里的话我们可以把整个数组一分为二,然后子数组也一分为二,不断划分下去就像这样:

c4f3dfdd7f9bdf0b3c2d89d609370f2b.png

然后呢?

然后问题才真正开始有趣起来,注意,当我们划分到最下层的时候,也就是不可再划分时会得到两个数组元素,比如对于数组[1,2]会划分出[1]与[2],此时[1]与[2]不可再划分,那么对于子问题[1,2],其最大子数组的和为max(1+2, 1,2),也就是说要么是左半部分的元素值、要么是右半部分的元素值、要么是两个元素的和,就这样我们得到了最后两层的答案:

14052c32927ee695f4407a35fb816c44.png

假设对于数组[1,2,3,4],一次划分后得到了[1,2]与[3,4],用上面的方法我们可以分别知道这两个问题的最大子数组和,我们怎样利用上述的答案来解决更大的问题,也就是[1,2,3,4]呢?

很显然,对于[1,2,3,4]来说,最大子数组的和要么来自左半部分、要么来自右半部分、要么来自中间部分——也就是包含2和3,其中左半部分和右半部分的答案我们有了,那么中间部分的最大和该是多少呢?

其实这个问题很简单,我们从中间开始往两边不断累加,然后记下这个过程的最大值,比如对于[1,-2,3,-4,5],我们从中间的3开始先往左边累加和是:{1+(-2)+3, (-2)+3, 3}也就是{2,1,3},因此我们以中间数字为结尾的最大子数组和为3:

f05bf5dbc42d23abc387cc6a7beaf8b4.png

另一边也是同样的道理,只不过这次是以中间数字为起点向右累加:

3a64c886e70c9d100cd889e6aaf61355.png

然后这三种情况中取一个最大值即可,这样我们就基于子问题解决了更大的问题:

e49c4fb1ae3d3e0f8edbc9432b1030be.png

此后的道理一样,最终我们得到了整个问题的解。

根据上面的分析就可以写代码了:

int getMaxSum(vector<int>& nums, int b, int e) {
    if (b == e) return nums[b];
    if (b == e - 1) return max(nums[b], max(nums[e], nums[b]+nums[e]));
    int m = (b + e) / 2;
    int maxleft = nums[m];
    int maxright = nums[m];
    int sum = nums[m];

    for (int i = m + 1; i <= e; i++) {
        sum += nums[i];
        maxright = max(maxright, sum);
    }

    sum = nums[m];
    for (int i = m - 1; i >= b; i--) {
        sum += nums[i];
        maxleft = max(maxleft, sum);
    }
    return max(getMaxSum(nums, b, m - 1), max(getMaxSum(nums, m + 1, e), maxleft+maxright-nums[m]));
}
int maxSubArray(vector<int>& nums) {
    return getMaxSum(nums, 0, nums.size()-1);
}

上述这段代码的时间复杂度为O(NlogN)比第二种方法又提高了很多。

动态规划

实际上这个问题还有另一种更妙的解决方法,我们令dp(i)表示以元素A[i]为结尾的最大子数组的和,那么根据这一定义则有:

5bf27bda0e60ac34e29424b7b3b8a151.png

这是很显然的,注意dp(i)的定义,是以元素A[i]为结尾的最大子数组的和,因此dp(i)的值要么就是A[i]连接上之前的一个子数组,那么不链接任何数组,那么最终的结果一定是以某个元素为结尾的子数组,因此我们从所有的dp(i)中取一个最大的就好了,依赖子问题解决当前问题的解就是所谓的动态规划。

有了这些分析,代码非常简单:

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

这段代码简单到让人难以置信,只有8行代码,你甚至可能会怀疑这段代码的正确性,但它的确是没有任何问题的,而且这段代码的时间复杂度只有O(N),这段代码既简单运行速度又快,这大概就是算法的魅力吧,关于动态规划后续在小风算法这个公众号里会有系统性总结,我保证你可以看懂

-End-

最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!

2063a0550811144ddf2ffe5048a70d3e.png

点击👆卡片,关注后回复【面试题】即可获取

在看点这里219ea99c63d50fc0e13044d5f29be759.gif好文分享给更多人↓↓

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
钉钉打卡是一种办公考勤软件,通常需要使用手机进打卡,但有时可能会出现一些问题,如因为手机权限限制或系统限制而无法正常打卡。为了解决这些问题,一些开发者提出了用两代码来解决钉钉打卡的方法,免除了手机的root权限,并且可以永久防封。下面是具体的操作步骤: 1. 首先,需要准备一个具有无障碍权限的手机,无障碍权限可以在手机的设置中找到,打开该权限,并允许相关应用获得该权限。 2. 然后,下载并安装一款名为Auto.js Runner的应用,可以从某些应用市场或者开发者官网获取安装包。 3. 打开Auto.js Runner应用,在应用中创建一个新的脚本,在脚本编辑界面中输入下面的两代码: ```javascript launchApp("钉钉"); click(100, 400); ``` 上述代码中,第一代码的作用是启动钉钉应用,第二代码的作用是模拟点击屏幕上的坐标(100, 400),该坐标应该是“打卡”按钮在屏幕上的位置,具体坐标可以根据手机屏幕的大小进调整。 4. 保存并运这个脚本,脚本会自动打开钉钉应用,并点击屏幕上的“打卡”按钮,完成打卡操作。 使用这种方法可以实现免root永久防封的打卡操作,而且不需要进复杂的设置或者额外的权限获取。但需要注意的是,由于每个手机的屏幕大小和分辨率不同,所以需要根据实际情况进坐标的调整,以确保脚本的正确运。此外,开发者还需要遵守相关法律法规,合法使用此类工具,并确保不会对他人造成不良影响。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值