分治策略 - 最大子序列问题

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分治策略 - 最大子序列问题相关的知识,希望对你有一定的参考价值。

  自开始学习算法起,我感觉就是跪着把《算法导论》的代码看一遍、理解一遍然后敲一遍...说实话自己来写并且要求时间复杂度达到要求,我肯定是不能做到的,但我想前辈们辛苦积累的研究成果贡献出来也是为了让后人少走一些弯路,所以我的作用就是把前辈们的成果学习之后加以理解,然后积累经验,领悟到他们解决问题时的思路和灵感。还有就是把个人理解后的知识存储在不会忘记的地方作为复习备用...

  当然什么是写博客呢,我个人认为是把所学的知识加上自己的理解然后用较为通俗的语言来解释一遍,至少这样才可能把学到的东西变为自己的东西(也有可能学习的时候以为自己懂了,然后就丢博客,之后就不管了,然后因理解不够而忘记了的情况)...

  还是进入主题吧,最近也一直在看分治问题,学到了一些东西。最大子序列问题是一个很经典的线性规划问题,我前面做过一个最长单调递增子序列的题,当时对算法的了解还只是“不管什么只要能解决问题就好行了”的程度...然后,当时也有看《数据结构与算法分析 第二版》这本书,当时着实把我吓了一跳...从O(n^3)降到O(n^2)再到O(n)...

  先说说最大子序列问题是个什么问题吧。最大子序列问题的描述是这样的:从一组数中找出下标连续的几个数,这几个组成的数组的和是所有情况中值最大数组(我文科不好,只能这么绕口的描述了)。比如:{13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7},这么一组数中最大的为:{18, 20, -7, 12},这组数的和是最大的:43。如果想要把所有情况都列出来找的话,这样的数组有A(n,n)种组合(包含了重复情况),太暴力了,输入规模很大时,时间复杂度为Θ(n!),去掉重复情况还有A(n,2)种,时间复杂度为Θ(n^2),这一样很慢。而《数据结构与算法分析 第二版》书中的算法4给出的代码是这样的:

#include<stdio.h>

int MaxSubarraySum(int * nums, int n) {
    int ThisSum, MaxSum, i;

    ThisSum = MaxSum = 0;
    for(i = 0; i < n; i++) {

        ThisSum += nums[i];          //每次循环则累加
        if(ThisSum > MaxSum)         //每次循环,如果成立则MaxSum的值就会变为ThisSum
            MaxSum = ThisSum;
        else if(ThisSum < 0)         //每次循环,如果成立则ThisSum重新置为0
            ThisSum = 0;
    }

    return MaxSum;
}

int main()
{
int arr[] = {-2, 11, -4, 13, -5, -2}, * nums = arr; int val; val = MaxSubarraySum(nums, sizeof(arr)/sizeof(int)); printf("MaxSubarraySum = %d.\n", val); return 0; } 运行结果:
MaxSubarraySum
= 20.

  其实并不难理解为什么会是正确的,用上面给出的数组{-2, 11, -4, 13, -5, -2}为例子来模拟一下:

  开始 ThisSum = 0,MaxSum = 0;进入循环,ThisSum = -2 < MaxSum,所以 ThisSum = 0,继续循环,ThisSum = 0 + 11 = 11 > MaxSum,所以 MaxSum = 11, 继续循环,ThisSum = 11 + (-4) = 7 < MaxSum, 因为ThisSum既不满足ThisSum > MaxSum,也不满足ThisSum < 0,ThisSum不变,所以继续循环,ThisSum = 7 + 13 = 20 > MaxSum, 所以 MaxSum = 20,继续循环,ThisSum = 20 + (-5) = 15,也不满足 ThisSum > MaxSum 和 ThisSum < 0,所以不变,继续循环,ThisSum = 15 + (-2) = 13,还是不满足两个条件判断,继续循环,然后 i > n,退出循环,返回MaxSum的值。最终MaxSum = 20。最坏时间复杂度为O(n)(ORZ...)...

  虽然上面这个算法很强...但本篇主要是讲解分治,所以顾不上膜拜了,然后在来看一下分治策略是如何解决这个问题的。

  先看代码:

#include<stdio.h>

static int Find_Max_Crossing_SubArray(int * nums, int start, int mid, int end) {

    int left = -65533, right = -65533;
    int sum;
    int i, j;

    sum = 0;
    for(i = mid; i >= start; i--) {
        sum += *(nums + i);
        if(sum > left) {
            left = sum;
        }
    }

    sum = 0;
    for(j = mid + 1; j < end; j++) {
        sum += *(nums + j);
        if(sum > right) {
            right = sum;
        }
    }

    return left + right;
}

int Find_Maximum_Subarray(int * nums, int start, int end) {

    int left;
    int right;
    int cross;
    int mid;

    if(start == end)
        return * nums;
    else
    {
        mid = (start + end)/2;
        left = Find_Maximum_Subarray(nums, start, mid);
        right = Find_Maximum_Subarray(nums, mid + 1, end);
        cross = Find_Max_Crossing_SubArray(nums, start, mid, end);
        
        if(left >= right && left >= cross)
            return left;
        else if(right >= left && right >= cross)
            return right;
        else
            return cross;
    }
}

int main()
{
    int arr[] = {13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7}, * nums = arr;
    int val;

    val = Find_Maximum_Subarray(nums, 0, sizeof(arr)/sizeof(int));
    printf("MaxSubarraySum = %d.\n", val);
    return 0;
}

运行结果:
MaxSubarraySum = 43.

  该如何理解这段代码呢?首先先来了解一下什么是分治策略。分治策略讲的是先用将问题分为几个部分,分出去的每个部分解决自己负责的那部分问题,然后再用来将所有分出去的部分合起来解决整个问题的一种策略。

  这样就好理解上面的这段代码了,那么该问题有几个子问题呢?答案是2个,cross只不过是负责了所有情况下分布在中间部分的附带问题,实际上它还是属于两个子问题中的一部分...详细说一下它负责的部分:第一次调用函数,它就开始负责一个完整的数组的中间部分,而left和right进入递归,第一次递归中cross负责了left-mid数组的部分的中间部分和right-end数组的部分的中间部分(注意,问题的规模被分为了两部分,2个子问题的规模各占一半),这样不断进入递归,从而最终left负责的部分的递归将得到left负责的这部分中的cross的返回值,right负责的部分的递归得到right负责的这部分中的cross的返回值,然后比较三个值(left、right、第一次函数调用时的cross)的大小,最大的作为函数的返回值返回。可能你还会问那其他子递归中的left和right怎么办?注意到还有个条件判断if(start == end) 如果满足则返回数组首地址的元素,这就说明了其他left、right部分的递归最终会满足这个条件,从而用来比较,如果都没有其他两个大肯定就被覆盖了...(不知道这样说得够不够清楚)。总之,还是得理解递归。

  然后我们来分析这个算法的时间复杂度,首先说一个分治策略问题的基本公式:T(N) = aT(N/b) +f(N) (其中a表示的是子问题个数,N/b表示的是子问题的规模)。

  上面的代码我们这样分析它的时间消费:

    1.函数Find_Max_Crossing_SubArray()中有两个循环start-mid,mid+1-end,数组nums[start...end]包含n个元素,所以(mid-start+1) + (end-mid) = n。初始sum赋值消费(1+1),不忽略循环条件中的判断和累加共消费(n/2+1+n/2+1),且每次循环sum都要累加共消费(n/2+n/2)和判断共消费(n/2+n/2),所以我们可得到整体共消费n+n+n+1+1+1+1 = 3n+4 = Θ(n),所以函数Find_Max_Crossing_SubArray()的时间复杂度为Θ(n)。

    2.①函数Find_Maximum_Subarray()中如果第一个判断成立消费(1)。对于n = 1的基本情况,T(1) = Θ(1)。

       ②n > 1时,第一个条件判断消费(1),两个函数自身调用,子问题规模为n/2,因此每个子问题的时间消费为T(n/2),有2个子问题,故总共消费2T(n/2),一个函数调用时间消费为Θ(n),后面的条件判断只消费了(1)。故可写出递归情况的消费为 T(n) = Θ(1)+2T(n/2)+Θ(n)+Θ(1) = 2T(n/2)+Θ(n)。

  综合①和②得到函数Find_Maximum_Subarray()的运行时间的递归式为:   

T(1) = Θ(1)             n = 1;
T(n) = 2T(n/2) + Θ(n)      n > 1;

  然后,我们再重写一下递归式:

T(1) = c               N = 1;
T(N) = 2T(N/2) + cN       N > 1;

  现在我们再来求解这个递归式的时间复杂度:

T(1) = c               N = 1;
T(N) = 2T(N/2) + cN       N > 1;

T(1) = c
T(2) = 2T(1) + 2c = 4c = 2*2c
T(4) = 2T(2) + 4c = 12c = 3*4c
...
T(N) = T(2^k) = n*N*c = (k+1)*N*c = (k+1)*(2^k)*c(令 n = k+1, k = 0,1,2,3...)
得到: N
= 2^k; k = lgN;
因为 n
= k+1, 所以 n = lgN + 1. 故 T(N) = Θ((k+1)*N) = Θ(NlgN+N) = Θ(NlgN). 证毕.

  然后,要注意记号Θ()、Ω()、O()的不同之处,Θ()包含Ω()和O(),O()记号常用来描述最坏情况时间复杂的,是一个上界;Ω()用来描述最好情况时间复杂度,是一个下界。

  最后,说明一下,证明和计算是自己想的(因为书上要求自己求解一下递归式),可能会有不正确的地方,还望指正! 关于最大子序列问题我学习得到的暂时就这么多了,学习能力有限,还望不吝指教。

以上是关于分治策略 - 最大子序列问题的主要内容,如果未能解决你的问题,请参考以下文章

0.分治永远大于顺序?关于最大子序列和问题的思考

分治策略---求最大子数组

分治法求最大子序列

分治策略   最大子数组问题

分治算法解最大子序列和问题

分治法 解决最大字段和问题