689. Maximum Sum of 3 Non-Overlapping Subarrays - Hard

本文探讨了一种高效算法,用于寻找给定数组中三个不重叠子数组的最大和,每个子数组长度固定,通过动态规划实现。文章详细介绍了算法的设计思路及其实现过程。

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

Description

In a given array nums of positive integers, find three non-overlapping subarrays with maximum sum.
Each subarray will be of size k, and we want to maximize the sum of all 3*k entries.
Return the result as a list of indices representing the starting position of each interval (0-indexed). If there are multiple answers, return the lexicographically smallest one.

Example:
Input: [1,2,1,2,6,7,5,1], 2
Output: [0, 3, 5]
Explanation: Subarrays [1, 2], [2, 6], [7, 5] correspond to the starting indices [0, 3, 5].
We could have also taken [2, 1], but an answer of [1, 3, 5] would be lexicographically larger.
Note:
nums.length will be between 1 and 20000.
nums[i] will be between 1 and 65535.
k will be between 1 and floor(nums.length / 3).

Analyze

子数组的和是很好求的,扫一遍就行,应该可以存下来备用。
这么一来那问题可以做一个转化了,从子数组和的这个数组中挑三个数使得和最大,且两个数之间至少有一定间隔。k=1直接取最大的三个,k=2至少隔一个取……
很贪心地,上来就先找到最大的三个,然后想办法做调整——这很符合我们人脑解决问题的思路。我试过了,但最后测试样例一大起来就看不出哪里有bug,不行。

某天突然想到,这三个数组左中右排开,其可探索的范围是有限的。就是说,在求过和的新数组中我要找到a、b、c三个位置,最合适的a并不需要跟很右边的数比较,右边如果有很大的数,自然会有c去占据它。
我知道这解释起来有点抽象,不过这的确就是使我思路转变的最重要的事情。如果不明白也没关系,可以先往下看,然后回过头来思考这种方法的正确性。
我们拿出三个游标a、b、c摆在新数组的初始位置上,即a = 0, b = k, c = 2*k,向右扫描。c向右挪动i步,b向右挪动j步,a向右挪动k步,我们就可以用一个三层循环遍历完所有的abc组合,计算他们的和得到最大的。并且存在一个这样的关系:k<=j<=i。这是正确的,但显然超时。

于是很自然地就会想到用动态规划降低复杂度。新状态跟前一状态是什么关系呢?c每向右移动1步,就产生些许新的abc组合情况,产生些许新的和,新的和有比当前更大的就要更新abc的位置了。

Solution

要把我们上面的思路用代码正确表达出来可得费一番功夫。
维护一个最大和maxABC,每次c向右移产生新的c和新的组合,则newABC=newC+MAX{新ab,原ab},要是newABC更大就得更新。对于新产生的AB组合如何找到最大的也可以用类似的方式,newAB=newB+MAX{新a,原a},而a只要在它的可达范围内取最大就好了。这有点像递归,要找到最大的abc,得先找到最大的ab,也就得先找到最大的a。
找到更大的组合我们得更新,这里面就有点绕了。c一定是更新到新移动的位置i,ab则要更新到使得AB和取最大的值,而并不一定是新产生的i-2k和i-k。同理如果b取了新产生的值i-k,则a不一定是i-2k。
我们讲到很多变量,但不是都要维护。考虑清楚计算的顺序和更新的值从哪来,我们就得到下面的代码。

Code

vector<int> maxSumOfThreeSubarrays(vector<int>& nums, int k) {
    int a = 0, b = k, c = 2*k;
    vector<unsigned> sums(nums.size()-k+1, 0);
    for(int i = 0; i <= nums.size()-k; ++i) {
        unsigned sum = nums[i];
        for(int j = 1; j < k; ++j) sum += nums[i+j];
        sums[i] = sum; 
    }

    unsigned maxABC = sums[a] + sums[b] +sums[c];
    unsigned maxAB = sums[a] + sums[b];
    unsigned maxA = sums[a];
    int maxAB_a = a, maxAB_b = b, maxA_a = a;
    for(int i = c+1; i < sums.size(); ++i) {
        if(sums[i-2*k] > maxA) {
            maxA = sums[i-2*k];
            maxA_a = i - 2 * k;
        }
        if(sums[i-k] + maxA > maxAB) {
            maxAB = sums[i-k] + maxA;
            maxAB_b = i - k;
            maxAB_a = maxA_a;
        }
        if(sums[i] + maxAB > maxABC) {
            maxABC = sums[i] + maxAB;
            c = i;
            b = maxAB_b;
            a = maxAB_a;
        }
    }

    vector<int> ret;
    ret.push_back(a);
    ret.push_back(b);
    ret.push_back(c);
    return ret;
}

Summary

忘了最初的思路和很多思考细节,因为这道题真的做了很久,疯狂提交了N遍,至少写了三个版本的代码。今天终于兜清了里面的逻辑然后AC,我感到非常欣慰,因为坚持没有向答案势力低头,最后写出来的代码非常工整易懂(至少我自己这么觉得哈哈),感觉比评论区的答案还好。

这道题难就难在要理清楚里面的逻辑,应该保存和维护什么值,以及如何更新值。
还有个没有深入实践的改进想法,当这个数组很长的时候,a和b后来移动到的位置都是c走过的了,就应该可以利用起这种历史纪录。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值