最大子数组之和

原文http://www.ahathinking.com/archives/120.html

一个有N个元素的整型数组arr,有正有负,数组中连续一个或多个元素组成一个子数组,这个数组当然有很多子数组,求子数组之和的最大值。例如:[0,-2,3,5,-1,2]应返回9,[-9,-2,-3,-5,-3]应返回-2。

网上有称之为最大子序列和,亦有称连续子数组最大和。个人觉得叫最大子序列和不太妥,数学上讲,子序列不一定要求连续,而这里我们的题目必然要求是连续的,如果不连续而求子序列最大和很显然就无意义了,这也是为啥又称连续子数组最大和。不过,莫要在意细节。

鉴于《编程之美》对其有几个扩展问题,这里就练手一并实现了,不过整理过程中发现了《编程之美》中的解法错误,查了一下官网的勘误表,居然木有,小激动了一下。。。本节包括以下内容:

==基本思路==

==DP方案==

==返回最大子数组始末位置==

==数组首尾相连【《编程之美》解法错误分析】==

==类似问题==

==================================

基本思路

最直接的方法就是找出所有的子数组,然后求其和,取最大。如果每个子数组都遍历求和,该方法的复杂度为O(N^3),仔细考虑,在遍历过程中,这些子数组的和是有重复计算的:下标i与j之间的区间和Sum[i,j]=Sum[i,j-1]+arr[j]。于是子数组和的求法不必每次都遍历,算法复杂度可以降为O(N^2)。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 最大子数组和 设数组长度不超过30*/
 
#define INF 1000
 
/* 基本思路 */
int Maxsum_base(int * arr, int size)
{
     int maxSum = -INF;
     for(int i = 0; i < size; ++i) /*for each i, got a sum[i,j]*/
     {
         int sum = 0;
         for(int j = i; j < size; ++j)
         {
             sum += arr[j];
             if(sum > maxSum)
             {
                 maxSum = sum;
             }
         }
     }
     return maxSum;
}

==================================

DP方案

考虑DP求解。这个问题可以继续像LCSLIS一样,“从后向前”分析(《编程之美》对此又是从前向后分析的,个人不太习惯)。我们考虑最后一个元素arr[n-1]与最大子数组的关系,有如下三种情况:

  1. arr[n-1]单独构成最大子数组
  2. 最大子数组以arr[n-1]结尾
  3. 最大子数组跟arr[n-1]没关系,最大子数组在arr[0-n-2]范围内,转为考虑元素arr[n-2]

从上面我们可以看出,问题分解成了三个子问题,最大子数组就是这三个子问题的最大值,现假设:

  1. 以arr[n-1]为结尾的最大子数组和为End[n-1]
  2. 在[0-n-1]范围内的最大子数组和为All[n-1]

如果最大子数组跟最后一个元素无关,即最大和为All[n-2](存在范围为[0-n-2]),则解All[n-1]为三种情况的最大值,即All[n-1] = max{ arr[n-1],End[n-1],All[n-2] }。从后向前考虑,初始化的情况分别为arr[0],以arr[0]结尾,即End[0] = arr[0],最大和范围在[0,0]之内,即All[0]=arr[0]。根据上面分析,给出状态方程:

1
All[i] = max{ arr[i],End[i-1]+arr[i],All[i-1] }

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* DP base version*/
#define max(a,b) ( a > b ? a : b)
 
int Maxsum_dp(int * arr, int size)
{
     int End[30] = {-INF};
     int All[30] = {-INF};
     End[0] = All[0] = arr[0];
 
     for(int i = 1; i < size; ++i)
     {
         End[i] = max(End[i-1]+arr[i],arr[i]);
         All[i] = max(End[i],All[i-1]);
     }
     return All[size-1];
}

上述代码在空间上是可以优化为O(1)的,这里就不说了,详参考《编程之美》2.14。

下面说一下由DP而导出的另一种O(N)的实现方式,该方法直观明了,个人比较喜欢,所以后续问题的求解也是基于这种实现方式来的。

仔细看上面DP方案的代码,End[i] = max{arr[i],End[i-1]+arr[i]},如果End[i-1]<0,那么End[i]=arr[i],什么意思?End[i]表示以i元素为结尾的子数组和,如果某一位置使得它小于0了,那么就自当前的arr[i]从新开始,且End[i]最初是从arr[0]开始累加的,所以这可以启示我们:我们只需从头遍历数组元素,并累加求和,如果和小于0了就自当前元素从新开始,否则就一直累加,取其中的最大值便求得解。

到这里其实就可以了,在《编程之美》中,作者故意没有按照这种推导来实现(我猜的),而是在End[i-1]<0时,让End[i]=0,从而留出了一个问题(元素全是负数怎么办),其实如果按照上面的推导直接实现的话,就不存在这个问题了;(题外话:还是要坚持写博客做总结,这是一个重新思考的过程,这几天做这几道题发现当时也就看懂大部分,更多的细节和问题是在写博客重新整理的过程中发现的。后面扩展问题也是在整理过程中才发现了《编程之美》中的错误。)

基于上面的推导,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DP ultimate version */
int Maxsum_ultimate(int * arr, int size)
{
     int maxSum = -INF;
     int sum = 0;
     for(int i = 0; i < size; ++i)
     {
         if(sum < 0)
         {
             sum = arr[i];
         }else
         {
             sum += arr[i];
         }
         if(sum > maxSum)
         {
             maxSum = sum;
         }
     }
     return maxSum;
}

其实上面的方法虽说是从DP推导出来的,但是写完发现也是很直观的方法,求最大和,那就一直累加呗,只要大于0,就说明当前的“和”可以继续增大,如果小于0了,说明“之前的最大和”已经不可能继续增大了,就从新开始,如此这样。

==================================

返回最大子数组始末位置

这个问题是《编程之美》2.14的扩展问题,返回始末位置还是比较容易的,我们知道,每当当前子数组和的小于0时,便是新一轮子数组的开始,每当更新最大和时,便对应可能的结束下标,这个时候,只要顺便用本轮的起始和结束位置更新始末位置就可以,程序结束,最大子数组和以及其始末位置便一起被记录下来了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 最大子数组 返回起始位置 */
void Maxsum_location(int * arr, int size, int & start, int & end)
{
     int maxSum = -INF;
     int sum = 0;
     int curstart = start = 0;  /* curstart记录每次当前起始位置 */
     for(int i = 0; i < size; ++i)
     {
         if(sum < 0)
         {
             sum = arr[i];
             curstart = i;     /* 记录当前的起始位置 */
         }else
         {
             sum += arr[i];
         }
         if(sum > maxSum)
         {
             maxSum = sum;
             start = curstart; /* 记录并更新最大子数组起始位置 */
             end = i;
         }
     }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值