最大子数组和
给你一个整数数组
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[k−1]+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[n−1]…+nums[n]+nums[0]+nums[1]+…+nums[j1]+nums[j]
注意到该子数组为new数组的一个子数组
i | 0 | k(k<n) | n-1 | n | m(m>n) | 2*n-1 |
---|---|---|---|---|---|---|
new | nums[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] sm−sn=new[m+1]+new[m+2]+…+new[n−1]+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,则需将队头元素删除(另外队列不为空队是第一前提).