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

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

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

  还是进入主题吧,最近也一直在看分治问题,学到了一些东西。最大子序列问题是一个很经典的线性规划问题,我前面做过一个最长单调递增子序列的题,当时对算法的了解还只是“不管什么只要能解决问题就好行了”的程度...然后,当时也有看《数据结构与算法分析 第二版》这本书,当时着实把我吓了一跳...从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()记号常用来描述最坏情况时间复杂的,是一个上界;Ω()用来描述最好情况时间复杂度,是一个下界。

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

时间: 2025-01-12 02:40:16

分治策略 - 最大子序列问题的相关文章

分治策略结合递归思想求最大子序列和

我的主力博客:半亩方塘 对于 <数据结构与算法分析--C语言描述> 一书第 20 页所描述的算法 3,相信会有很多人表示不怎么理解,下面我由具体问题的求解过程出发,谈谈我自己的理解: 首先,什么是分治法呢?所谓 分治法,就是 将一个问题的求解过程分解为两个大小相等的子问题进行求解,如果分解后的子问题本身也可以分解的话,则将这个分解的过程进行下去,直至最后得到的子问题不能再分解为止,最后将子问题的解逐步合并并可能做一些少量的附加工作,得到最后整个问题的解.在求解原来整个问题的算法思想,与求解每一

南邮算法分析与设计实验1 分治策略

分治策略 实验目的: 理解分治法的算法思想,阅读实现书上已有的部分程序代码并完善程序,加深对分治法的算法原理及实现过程的理解. 实验内容: 用分治法实现一组无序序列的两路合并排序和快速排序.要求清楚合并排序及快速排序的基本原理,编程实现分别用这两种方法将输入的一组无序序列排序为有序序列后输出. 代码: #include <iostream> #include <cstdlib> #include <ctime> using namespace std; void Swa

【从零学习经典算法系列】分治策略实例——快速排序(QuickSort)

在前面的博文(http://blog.csdn.net/jasonding1354/article/details/37736555)中介绍了作为分治策略的经典实例,即归并排序,并给出了递归形式和循环形式的c代码实例.但是归并排序有两个特点,一是在归并(即分治策略中的合并步骤)上花费的功夫较多,二是排序过程中需要使用额外的存储空间(异地排序算法<out of place sort>). 为了节省存储空间,出现了快速排序算法(原地排序in-place sort).快速排序是由东尼·霍尔所发展的一

【从零学习经典算法系列】分治策略实例——二分查找

1.二分查找算法简介 二分查找算法是一种在有序数组中查找某一特定元素的搜索算法.搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束:如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较.如果在某一步骤数组 为空,则代表找不到.这种搜索算法每一次比较都使搜索范围缩小一半.折半搜索每次把搜索区域减少一半,时间复杂度为Ο(logn). 二分查找的优点是比较次数少,查找速度快,平均性能好:其缺点是要求待查表为有序表,且

第四章 分治策略 4.1 最大子数组问题 (暴力求解算法)

/** * 最大子数组的暴力求解算法,复杂度为o(n2) * @param n * @return */ static MaxSubarray findMaxSubarraySlower(int[] n) { long tempSum = 0; int left = 0; int right = 0; long sum = Long.MIN_VALUE; for (int i = 0; i < n.length; i++) { for (int j = i; j < n.length; j++

第四章 分治策略 4.1 最大子数组问题 (减治法,别人的,拿来看看)

/** * 获得连续子数组的最大和 * * @author dfeng * */ private static long getMax(long a, long b) { return a > b ? a : b; } /** * 获得连续子数组的最大和 * * @param array * @return 最大和,此处用了Long型是为了表示当参数为null或空时,可以返回null,返回其它任何数字都可能引起歧义. */ public static Long getMax(int[] arra

分治策略

 分治策略分为三步: 分解原问题:将原问题分解为一些子问题,子问题形式与原问题一样,只是规模更小. 解决子问题:递归的求解出子问题.如果子问题规模足够小,则停止递归,直接求解. 合并子问题:将子问题的解合并为原问题的解 主方法公式:T(n)=aT(n/b)+f(n);它刻画了这样一个分治算法:生成a个子问题,每个子问题的规模是原问题的1/b,分解合并子问题时间为f(n);

第四章 分治策略——最大子数组问题

最大子数组问题 方法一:暴力求解方法 我们可以很容易地设计出一个暴力方法来求解本问题:简单地尝试没对可能的子数组,共有O(n2)种 #include<iostream> using namespace std; #define INT_MIN 0x80000000 int main() { int arr[10]={9,8,-3,-5,7,-39,79,-37,8,9}; int i,j; int sum=0,maxsum=INT_MIN; int imax; for(i=0;i<10;

【经典算法】分治策略

一.什么是分治 有很多算法是递归的:为了解决一个给定的问题,算法要一次或多次递归调用其自身来解决的子问题.这些算法通常采用分治策略:将原问题划分为n个规模较小而结构与原问题相似的子问题:递归地解决这些子问题,然后再合并其结果,就得到原问题的解. 二.分治算法的三个步骤 分治模式在每一层递归上都有三个步骤: 分解(Divide)步骤将问题划分为一些子问题,子问题的形式与原问题一样,只是规模更小. 解决(Conquer)步骤递归地求解出子问题.如果子问题规模足够小,则停止递归,直接求解. 合并(Co