子数组的最大和

文章讲述了如何通过动态规划结合双端队列的数据结构来解决最大连续子数组和的问题,特别强调了在环形数组中寻找非空子数组的最大和,以及如何利用前缀和的概念优化算法效率。
摘要由CSDN通过智能技术生成

最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组是数组中的一个连续部分。

动态规划

//以下代码为求出n长数组中,最大连续子数组和。
#define max(x, y) (((x) > (y))?(x):(y))//求出两数中较大者
int maxSubArray(int* nums, int numsSize) {
    int n = numsSize;
    int ans = nums[0];
    int temp = nums[0];
    for (int i = 1; i < n; i++) {
        temp = max(temp + nums[i], nums[i]);//temp[i] = max(temp[i-1] + nums[i], nums[i]);
        ans =  max(temp, ans);
    }
    return ans;
}

temp= max(temp+nums[i], nums[i]);为便于比较各循环内temp值,作出以下为temp做出标记。

为便于陈列,规定max(n1,n2,n3,n4)为取出其中最大数.

在 i = 1 时,有 t e m p [ 1 ] = m a x ( n u m s [ 0 ] + n u m s [ 1 ] , n u m s [ 1 ] ) ; 在i=1时,有temp[1]=max(nums[0]+nums[1], nums[1]); i=1时,有temp[1]=max(nums[0]+nums[1],nums[1]);
i = 2 时,有 t e m p [ 2 ] = m a x ( t e m p 1 + n u m s [ 2 ] , n u m s [ 2 ] ) = m a x ( n u m s 0 + n u m s [ 1 ] + n u m s [ 2 ] , n u m s [ 1 ] + n u m s [ 2 ] , n u m s [ 2 ] ) i=2时,有temp[2]=max(temp1+nums[2], nums[2]) =max(nums0+nums[1]+nums[2], nums[1]+ nums[2], nums[2]) i=2时,有temp[2]=max(temp1+nums[2],nums[2])=max(nums0+nums[1]+nums[2],nums[1]+nums[2],nums[2])
同样的的 i = k 时 , 有 t e m p [ k ] = m a x ( n u m s [ 0 ] + n u m s [ 1 ] + … n u m s [ k ] , n u m s [ 1 ] + n u m s [ 2 ] + n u m s [ k ] , … , n u m s [ k − 1 ] + n u m s [ k ] , n u m s [ k ] ) ; 同样的的i=k时,有temp[k] = max(nums[0]+nums[1]+…nums[k],nums[1]+nums[2]+ nums[k], …, nums[k-1]+nums[k],nums[k] ); 同样的的i=k,temp[k]=max(nums[0]+nums[1]+nums[k],nums[1]+nums[2]+nums[k],,nums[k1]+nums[k]nums[k]);

可以看出max内参数实际上不符合k项前所有连续子数组的定义,其实际为包含第k项的连续子数组(且该子数组内元素下标不超过k),又有集合:{包含第0项的连续子数组,包含第1项的连续子数组………包含第k项的连续子数组,包含第n-1项的连续子数组}

该集合即为n长数组的连续子数组。因此实际上所求为结果max(temp[1],tmep[2],temp[k]…,temp[n-1]);

故引出一个变量ans,在每次循环中取得temp中最大值即可。(注意到nums[0],并未给出判断,因此将ans初始化为nums[0])

而在循环中有temp[i] = max([i-1]+nums[i], nums[i]);

那么temp[i]只与temp[i-1]有关(nums[i]视作常数),则可以将temp[i] = max(temp[i-1] + nums[i], nums[i])优化为temp = max(temp + nums[i], nums[i]);

再看一道类似题目.

环形子数组的最大和

给定一个长度为 n环形整数数组 nums ,返回 nums 的非空 子数组 的最大可能和

环形数组 意味着数组的末端将会与开头相连呈环状。形式上, nums[i] 的下一个元素是 nums[(i + 1) % n]nums[i] 的前一个元素是 nums[(i - 1 + n) % n]

子数组 最多只能包含固定缓冲区 nums 中的每个元素一次。形式上,对于子数组 nums[i], nums[i + 1], ..., nums[j] ,不存在 i <= k1, k2 <= j 其中 k1 % n == k2 % n

问题转化,在普通数组中,子数组则为连续的数组元素,此时存在循环情况,我们则需考虑特殊情况的数组形式,设该子数组首元素为a[i],尾元素为a[j],n为nums数组长度则特殊情形为

n u m s [ i ] + n u m s [ i + 1 ] + n u m [ i + 2 ] + n u m s [ n − 1 ] … + n u m s [ n ] + n u m s [ 0 ] + n u m s [ 1 ] + … + n u m s [ j 1 ] + n u m s [ j ] nums[i] + nums[i+1] +num[i+2]+nums[n-1]…+nums[n]+nums[0]+nums[1]+…+nums[j1]+nums[j] nums[i]+nums[i+1]+num[i+2]+nums[n1]+nums[n]+nums[0]+nums[1]++nums[j1]+nums[j]

注意到该子数组为new数组的一个子数组

i0k(k<n)n-1nm(m>n)2*n-1
newnums[0]nums[k]nums[n-1]nums[n%n]nums[m%n]nums[(2*n-1)%n]

(小于n部分的下标k也可以转化为k%n)即有这样的映射**(new[i] = nums[i%n])**

那么可以理解为new数组为两个nums数组拼接而形成的普通数组。(第一个nums数组的尾部拼接第二个nums数组的头部)

nums[i] 的下一个元素是 nums[(i + 1) % n]nums[i] 的前一个元素是 nums[(i - 1 + n) % n]

也可以从题中这部分提炼出。

问题就转化为在2*(n-1)的大小数组中找到长度小于等于n的连续子数组即可。那我们能否使用上面的方式继续做这道题呢?

当i = n+1,temp[n] = max(nums[0]+nums[1]+…nums[n],nums[1]+nums[2]+ nums[n], … nums[k-1]+nums[n],nums[n] );

而题中期望的的temp[n] =max(nums[1]+nums[2]+ nums[n], … nums[k-1]+nums[n],nums[n] );(即原解法第一个子数组需要剔除,另外如果在该次循环中记录下不同个最大值,则在2n次会浪费极大的空间开销,同时在调用这些最大值时,也需要遍历这个数数组,与暴力解法别无二致)

那我们需要引入新的概念,前缀和:数组元素从第0项,第1项,…,第n项元素的和,记作

s x = n e w [ 0 ] + n e w [ 1 ] + n e w [ 2 ] + … + n e w [ x ] s_x = new[0]+new[1]+new[2]+…+new[x] sx=new[0]+new[1]+new[2]++new[x]

s m − s n = n e w [ m + 1 ] + n e w [ m + 2 ] + … + n e w [ n − 1 ] + n e w [ n ] s_m- s_n = new[m+1]+ new[m+2]+…+new[n-1]+new[n] smsn=new[m+1]+new[m+2]++new[n1]+new[n]

那么在new中任意不超过n长的子数组都可以表示为s_m - s_n , (0<m-n < n);长度表示为(m-n)

s_i我们可以动态的求得,那我们求子数组和最大,就需要减数(s_n)尽可能地小,同时满足(m - n <= n).

我们则引入新的数据结构

双端队列:双端队列不仅仅局限于队列的先进先出的特点,同时增加了的后进先出(类似于栈的特点).即在队尾也进行删除队列元素的操作,队头增加数据的操作.

int maxSubarraySumCircular(int* nums, int numsSize) {
    int n = numsSize;
    int doubleq[n * 2][2];
    int head = 0, tail = 0, temp = nums[0], ans = nums[0];
    doubleq[tail][0] = 0;
    doubleq[tail++][1] = nums[0];
    for (int i = 1; i < 2 * n; i++) {
        while (head != tail && i-doubleq[head][0] > n) {
            head++;
        }
        temp += nums[i % n];//这里原本为new[i],但考虑到上文的映射,作简化处理
        ans = ans > (temp - doubleq[head][1]) ? ans : temp - doubleq[head][1];
        /*即为表达式(s_m - s_n),注意不了解子数组的构成,可能最大的子数组和为负数,若将ans初始化为0,则会出现错解,故简单处理为nums[0]*/
        while (head != tail && doubleq[tail - 1][1] >= pre) {
            //尾指针是tail++,故tail始终指示队尾元素的下一元素,如需取得队尾元素,给tail减1,
            tail--;//删除队尾元素.
        }
        doubleq[tail][0] = i;
        doubleq[tail++][1] = pre;
    }
    return ans;
}

doubleq中元素构成,doubleq的第0列用于记录s_n中的n,即为数组new的下标索引,第1列用于记录此时的前缀和s_n的值.

那么何时需要删除队尾元素呢,

在第p次循环中,pre为第p项的前缀和.由于循环是从第1个元素开始累加,则下标较小的前缀和先得出,隐式的说明了tail指示为第q项的值在第i次循环前前已存入,则(q<p),同时队列中元素总是作为减数,尽可能地小,即下标越大(s_q先于s_p入队,则s_q先于s_p出队),前缀和值越小的更趋于最优解.因此只要有pre<=double[tail-1[1],(相等时p有第一优势,后进后出)时,直接删除第q项元素.

注意存在趋向最优解情况的先决条件为(第p项前缀和<=第q项前缀和),如果(第p项前缀和>第q项前缀和),直接入队即可

由此我们可以看出队列中从队头到队尾元素处于严格的单调递增,那么该数据结构也为单调队列.

代码中(i - doubleq[head[0] > n)这一条件意为如果子数组长度大于n,则需将队头元素删除(另外队列不为空队是第一前提).

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值