最大子数组问题

1. 问题描述

最大子数组问题,即给定一个数组A,找到A中和最大的非空连续子数组。
例如:有数组A = {1, -2, 3, 4, -3, -2 }, 则其中的最大子数组为A[2..3],其和为7.

本文提出了两种求解最大子数组问题的方法:分治策略与动态规划。其中,分治策略的时间复杂度为Θ(nlgn), 动态规划的时间复杂度为O(n)。

最后,分析了最大子数组问题的扩展问题的求解方法。

2求解方法

2.1分治策略

2.1.1分治概述

分治策略中,我们递归的求解一个问题,在每个问题中应用如下三个步骤:
1. 分解 将问题划分为一些子问题,子问题的形式与原问题一样,只是问题规模更小。
2. 解决 递归的求解出子问题。如果子问题的规模足够小,则停止递归,直接求解。
3. 合并 将子问题的解组合成原问题的解。

2.1.2分治求解

要寻找数组A的最大子数组,使用分治策略则需要将数组A划分为两个规模尽量相等的子数组。然后考虑求解两个子数组A[low,…,mid]和A[mid+1,…,high]。那么,A的任何连续子数组A[i,…,j]所处的位置必然是以下三种情况之一:

  • 完全位于子数组A[low,…,mid]中,因此low<= i <= j <= mid。
  • 完全位于子数组A[mid+1,…,high]中,因此mid< i <= j <= high。
  • 跨越了中点mid, 因此low <= i <= mid < j <= high。

对于第一种与第二种情况,我们可以递归求解,因为这两个子问题依然是最大子数组问题,只是规模更小。
而对于第三种情况,我们很容易在线性时间内求出跨越中点的最大子数组,因为它必须跨越中点,所以我们秩序找出形如A[i,…,mid]和A[mid+1,…,j]的最大子数组,然后将其合并即可。

跨越中点的最大子数组–伪代码:

Find-Max_Crossing-SubArray(A, low, mid, high)
left-sum = -∞
sum = 0
for i = mid downto low
    sum = sum + A[i]
    if sum > left-sum
        left-sum = sum
        max-left = i
right-sum = -∞
sum = 0
for j = mid+1 to high
    sum = sum + A[j]
    if sum > right-sum
        right-sum = sum
        max-right = j
return(max-left, max-right, left-sum + right-sum)

第1-7行求出了左半部分的最大子数组,因为必须包含mid,第3-7行的for循环从mid开始,递减到lowleft-sum保存目前为止找到的最大和,sum保存A[i,…mid]中的所有值,max-left记录下表i。同理,第8-14行求出了右半部分的最大子数组。
注意,如果A[low,…,high]包含n个元素,则调用Find-Max_Crossing-SubArray将花费Θ(n)的时间.

最大子数组的分治算法–伪代码:

Find-Maximun-SubArray(A, low, high)
if high == low
    return (low, high, A[low])
else mid = (low + high) / 2
    (left-low, left-high, left-sum) = Find-Maximun-SubArray(A, low, mid)
    (right-low, right-high, right-sum) = Find-Maximun-SubArray(A, mid + 1, high)
    (cross-low, cross-high, cross-sum) = Find-Max_Crossing-SubArray(A, low, mid, high)
    return (max(left-sum, right-sum, cross-sum))

第1行测试基本情况,即子数组只有一个元素的情况,该测试很重要。
第4-5行递归求解左右子数组中的最大子数组,第6-7行完成合并工作。

该分治算法的运行时间T(n) = Θ(nlgn)。

2.2 动态规划

当我们加上一个正数时,和会增加;当我们加上一个负数时,和会减少。如果当前得到的和是个负数,那么这个和在接下来的累加中应该抛弃并重新清零,不然的话这个负数将会减少接下来的和。

基于这样的思路,我们有代码:

/* 最大子数组 返回起始位置 */
int Maxsum_SubArray(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;
        }
    }
    return maxSum;
}

该算法的运行时间为T(n) = O(n),且空间上为O(1)。

3. 问题扩展

Ps:参考自 http://www.ahathinking.com/archives/120.html.

如果数组arr[0],…,arr[n-1]首尾相邻,也就是允许找到一段数字arr[i],…,arr[n-1],arr[0],…,a[j],使其和最大,该如何?

编程之美解法:这个问题的解可以分为两种情况:
1) 解没有跨越arr[n-1]到arr[0] (原问题)
2) 解跨越arr[n-1]到arr[0]

分析有:
数组A[] = {1,-2,3,5,-1,2}中的最大子数组为【1 | 3, 5, -1, 2】,即去掉了-2.
数组B[] = {8,-10,60,3,-1,-6}中的最大子数组为【8 | 60, 3, -1, -6】, 即去掉了-10.

这两个数都是两个数组中“最小”的。
所以,我们找最大子数组的对偶问题——最小子数组,有了最小子数组的值,总值减去它即可得到跨界的最大子数组(如果结果确实跨界的话)。

因此,在允许数组跨界(首尾相邻)时,最大子数组的和为下面的最大值。
Maxsum={ 原问题的最大子数组和;数组所有元素总值-最小子数组和 }

代码为:

/* 如数组首尾相邻 */
int Maxsum_endtoend(int * arr, int size)
{
    int maxSum_notadj = Maxsum_SubArray(arr,size); /* 不跨界的最大子数组和 */
    if(maxSum_notadj < 0)
    {
        return maxSum_notadj;          /* 若全是负数,则不会跨界 */
    }
    int maxSum_adj = -INF;             /* 跨界的最大子数组和 */
    //以下求最小子数组和
    int totalsum = 0;                  /* 总值 */
    int minsum = INF;
    int tmpmin = 0;
    for(int i = 0; i < size; ++i)      /* 最小子数组和 道理跟最大是一样的 */
    {
        if(tmpmin > 0)
        {
            tmpmin = arr[i];
        }else
        {
            tmpmin += arr[i];
        }
        if(tmpmin < minsum)
        {
            minsum = tmpmin;
        }
        totalsum += arr[i];
    }
    maxSum_adj = totalsum - minsum;
    return maxSum_notadj > maxSum_adj ? maxSum_notadj : maxSum_adj;
}

此时,该算法的时间复杂度为T(n) = O(n)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值