`
frank-liu
  • 浏览: 1684086 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

连续子序列最大和与乘积问题的分析

阅读更多

问题描述

        给定(可能是负的)整数序列A1, A2,...,AN, 寻找(并标识)使Sum(Ak)(k >=i, k <= j)的值最大的序列。如果所有的整数都是负的,那么连续子序列的最大和是零。

  对应的乘积问题则要求同样求出连续子序列中乘积最大的部分。

  我们这里针对最大和与最大乘积的问题分别进行讨论。

 

最大和

最简单暴力的解法

        这个问题有一个最简单直接的穷举解决法。我们看问题,既然要求里面最大的连续子序列。那么所有的连续子序列将由哪些组成呢?以数组的第一个元素为例,连续子序列必须是至少包含元素A1,也可能包含从A1到A2...以及从A1到AN。这样就有N种可能。后面的元素也按照这样类似的办法,以该元素开始,包含该元素的单元素数组,两个元素数组...直到包含数组末尾的数组。

        基于上面的分析,我们可以采用一个两重的循环,假设两个循环的循环变量分别为i, j。第一层循环从第一个元素遍历到后面,第二个元素从>=第一个元素的位置开始到最后。这样就可以遍历到所有的点。然后,我们取所有从i到j的连续数组部分和再比较。这样最终就可以得到最大的连续和以及最大子序列的起始与结束点。

具体的实现代码如下:

 

public static int maxSubSum1( int [ ] a )
{
    int maxSum = 0;

    for( int i = 0; i < a.length; i++ )
        for( int j = i; j < a.length; j++ )
        {
            int thisSum = 0;

            for( int k = i; k <= j; k++ )
                thisSum += a[ k ];

            if( thisSum > maxSum )
            {
                maxSum   = thisSum;
                seqStart = i;
                seqEnd   = j;
            }
        }

    return maxSum;
}

 笼统的来说,这种方法有一个3重循环,时间复杂度有O(N^3)。

 

改进

        前面那个最简单暴力的方法虽然看起来能解决问题,但是循环遍历的次数太多了。里面还是有不少可以改进的空间。比如说,每次我们用变量k遍历i到j的时候,都要计算求和。实际上当每次j增加1时,k把前面计算的结果在循环里又计算了一遍。这是完全没必要的,完全可以重复利用前面的计算结果。这样,通过这么一点改进,我们可以得到如下的算法代码:

 

public static int maxSubSum2( int [ ] a )
{
    int maxSum = 0;

    for( int i = 0; i < a.length; i++ )
    {
        int thisSum = 0;
        for( int j = i; j < a.length; j++ )
        {
            thisSum += a[ j ];

            if( thisSum > maxSum )
            {
                maxSum = thisSum;
                seqStart = i;
                seqEnd   = j;
            }
        }
    }

    return maxSum;
}

 

这种方法更进一步的在于,没必要在i和j之间进行遍历,所以时间复杂度为O(N^2)。对于每个外围循环i来说,当内层的循环j走完一遍,则获得了从i开头到j结束的所有子序列中最大的那个。

 

线性算法

        这个问题还是存在着一个线性时间复杂度的解法。需要我们对数组的序列进行进一步的分析。我们在数组中间找到的连续子序列,可能存在和为负的序列。如果需要找到一个最大的子数组的话,肯定该序列不是在最大子序列当中的。我们可以通过反证的方式来证明。

    假设数组A[i...j],里面的元素和为负。如果A[i....j]在一个最大子序列的数组中间,假定为A[i...k],k > j。那么既然从i到j这一段是负的,我把这一段去掉剩下的部分完全比我们假定的这个最大子序列还要大。这就和我的假设矛盾了。

    这个假设还有一个限制,就是该数组就是从i开头的。否则有人可能会这么问,如果我A[i...j]这一部分确实是一个负数,但是在A[i]前面是一个很大的正数,使得他们的和为正数。那不就使得我们的结果不成立了么?如果我们是从某个数组的开头i开始的话,就不存在这个情况。

        结合前面的讨论,我们就可以发现一个有意思的事情,就是假设我们从数组的开头A[0]开始,不断的往后面走,每一步判断是否当前和最大,并保存结果。当发现当前字串和为负数的时候,我们可以直接跳过。假设当前的索引为i的话,从0到i这一段的和是负数,可以排除。然后再从当前元素的后面开始找。这样可以得到最终最大子串和以及串的起点和终点。

详细的实现代码如下:

 

public static int maxSubSum3( int [ ] a )
{
    int maxSum = 0;
    int thisSum = 0;

    for( int i = 0, j = 0; j < a.length; j++ )
    {
        thisSum += a[ j ];

        if( thisSum > maxSum )
        {
            maxSum = thisSum;
            seqStart = i;
            seqEnd   = j;
        }
        else if( thisSum < 0 )
        {
            i = j + 1;
            thisSum = 0;
        }
    }

    return maxSum;
}

该方法只需要遍历数组一遍,通过跳过这些中间和为负的结果。算法时间复杂度为O(N).

 

乘积问题

  因为要求一个数组里连续子元素乘积的最大值。从初步的思路来说,可能会有这么一个初步的思路。一个数组里可能含有正数,负数,零这几个。对于包含零在内的元素乘积来说,肯定就是0了,这种情况比较特殊。作为最大元素乘积的话,应该尽量考虑取避免包含0,除非所有的乘积都是负数。而对于那些不包含零的序列来说,最大的序列可能为所有正数的乘积,也可能包含有若干个负数。因为负负得正,只要包含有偶数个负数也可能称为最大值。

  按照刚才的初步讨论,我们可以得到一个比较简单直观的方法。

 

划分法

  在前面的讨论,我们知道,最大连续乘积更可能出现在不包含零的序列里。所以我们完全可以以零作为隔断点,讲整个数组划分成若干个子段。这样,整个数组的连续最大乘积要么出现在这若干个子段里,要么就是零。

  如图中所示,我们要做的就是首先找到里面所有的零,将它们作为分割点,然后再去每个分割段里找里面的最大连续乘积。对于这些字段来说,它们不会包含有零,仅可能为若干个正负数的组合。一种求里面所有连续字段最大乘积的办法就是通过两轮循环去遍历所有的可能,然后保存这个子段里最大的值。按照这种思路,实现的代码如下:

public int maxInSegment(int[] a, int l, int r) {
    int max = 0;
    int product = 1;
    for(int i = l; i <= r; i++) {
        product = 1;
        for(int j = i; j <= r; j++) {
            product *= a[j];
            if(product > max)
              max = product;
        }
    }

    return product;
}

    这部分代码因为有一个两重遍历,它的时间复杂度为O(N^2)。能解决这个问题,只是不算效率很高。

   前面取零,并记录它们所在位置的实现可以通过一个LinkedList,每次碰到一个零就将零所在的索引加入到里面。以后每次遍历这个列表,将前后两个元素间的元素作为maxInSegment方法的参数传入来调用取得最大乘积。这部分的代码并不复杂,就不详述了。

  这种划分的方法虽然解决了问题,但是效率并不高。问题的关键点在于每次得到一个子段的时候,我们是通过蛮力取遍历所有的可能来求最大值。假设我们有一个子段a[i..k]是最大子段中间的一部分,在前面的某一次循环中已经计算过了,可是在后面的循环中还是会计算一遍,这样就浪费了不少时间。如果在这方面有所改进的话,后面的方法应该可以更好。

  那么,我们还又没有别的办法呢?

 

递推归纳法

  这里还有一种思考问题的思路。既然我们要求一个数组里连续子序列的最大乘积,那么对于一个数组,假设a[i..j]来说,它的最大连续乘积可能是如下图这样的一个形式:

    这里面的最大值是一个以下标k为结尾的连续子序列。从全局的角度来看,很明显,最终的这个最大连续乘积必然是一个包含某个元素为结尾的子串。只是这儿最大的子串不一定就是从数组的第一个元素作为开头。假如我们从数组开头i到结尾j的范围,求出到所有元素为结尾的子序列最大值,取其中最大的那个,这不就是我们所要求的那个最大连续子序列乘积吗?

  前面的这个问题概括起来不就是max(a) = max(max(a, i, i), max(a, i, i+1), ...,max(a, i, j)) 这样的一个等式吗?我们这里max(a, i, j)表示从数组i开始到j结束的范围内,包含j作为结尾的最大连续子序列乘积。

  如果我们需要定义一个递归的关系,在这个问题里,有可能的是max(a, i, k),它表示的是从i到k的范围内,包含k的一个连续子序列乘积。那么对于它后面的max(a, i, k+1)来说,它们会是一个什么关系呢?它们的递推关系可能会满足如下的几种情况:

1.max(a, i, k)和a[k+1]都是正数且max(a, i, k) * a[k+1] > max(a, i, k)

    在这种情况下,我们取得max(a, i, k) * a[k+1]作为max(a, i, k+1)的值。

2.  max(a, i, k)一个为正,一个为负

  在这种情况下,后面的元素a[k+1]因为已经是负数,如果和前面的max(a, i, k)相乘的话,得到的值会更小。而且,因为max(a, i, k+1)必须要包含a[k+1]在内,所以最大的这个值就是a[k+1]元素本身。同样,如果前面max(a, i, k)为负,a[k+1]为正,也是一样的结果。

3.max(a, i, k)为正,a[k+1]为负,不过max(a, i, k)之前相连的序列里有负数,比如:

    这个时候,我们会发现a[k+1] * max(a, i, k)不一定是最大的值。在这个示例中,如果再乘上前面的-2得到的结果才是最后最大的。而前面包含-2的这个序列,必然是一个前面序列里的最小值。同样,如果前面的序列里max(a, i, k)是负数,而且a[k+1]也是负数,则它们的直接乘积就是最大值。

  概括起来就是说包含第k+1个元素为结尾的序列最大乘积应该取自上述三种情况之一:

max(k+1) = max(maxk * a[k+1], mink * a[k+1], a[k+1])。

  按照同样的道理,我们求得的包含k+1在内结尾的最小乘积序列为:

min(k+1) = min(mink * a[k+1], maxk * a[k+1], a[k+1])

    这样,在求后面整个数组序列里的最大连续乘积的时候,就需要求出包含每个元素为结尾的序列里最大乘积值,然后通过逐步的比较求出最大的那个。详细实现的代码如下:

 

public static int maxProduct(int[] a) {
        int maxCur = 1;
        int minCur = 1;
        int maxTmp = maxCur;
        int minTmp = minCur;
        int result = 0;
        for(int i = 0; i < a.length; i++) {
            maxTmp = Math.max(a[i], Math.max(maxCur * a[i], minCur * a[i]));
            minTmp = Math.min(a[i], Math.min(maxCur * a[i], minCur * a[i]));
            maxCur = maxTmp;
            minCur = minTmp;
            result = Math.max(result, maxCur);
        }

        return result;
    }

 

 

总结分析

        这是一个比较有意思的问题。以前和朋友们曾经讨论过。当然,是在没看过书上的那么多分析之后自己也想到了一个近似O(N)的解法。当时还利用了一种情形,将所有为相邻为正的元素以及为负的元素分别累加起来构成一个新的数组。因为如果要达到最大的子数组,要么就要覆盖所有相邻的正整数,要么会包含一段相邻的负整数子序列。然后形成一个正负数相间的数组。这样再利用前面的线性算法特性进行比较。虽然不如前面的好,但是这是自己思考的结果,总是有价值的。

        另外,在某些情况下会出现上述问题的一个变种,就是假设我们需要支持负数的情况。最坏的情况就是所有元素都是负的,那么就相当于在里面找到最大的那个元素。我们的代码就需要稍微做一点修改。至于怎么改,如果你看明白了分析和代码的话,你懂的:)

        还有一个需要说明一点的就是,在实现的代码中用到了seqStart和seqEnd两个变量。可以将这两个元素定义为类的static变量。这样就构成一个完整的程序了。

   对于乘积问题来说,它的递归方法的一个要点是要根据以某个元素为结尾的序列和它后面相邻元素为结尾的连续序列的关系推导。max(k+1) = max(max(k)* a[k+1], min(k) * a[k+1], a[k+1])。只要这个关系能推导出来就好办。当然难也就难在怎么推导出这么个关系来。

 

参考资料

Data structures and problem solving using java

  • 大小: 1.7 KB
  • 大小: 3.9 KB
  • 大小: 4.1 KB
  • 大小: 4.1 KB
  • 大小: 4.3 KB
分享到:
评论

相关推荐

    利用C语言来求最大连续子序列乘积的方法

    标题中的知识点是利用C语言求解最大连续子序列乘积,描述中提到这是一个与ACM竞赛相关的题目,涉及浮点数序列。标签指出是关于C语言和子序列的问题。部分内容详细介绍了三种解法,分别是穷举法、特殊处理负数和0的...

    php-leetcode题解之乘积最大子序列.zip

    在这个问题中,目标是找出数组中的一个连续子数组,使得其乘积最大。 首先,我们要了解这个问题的基本定义。假设有一个整数数组`$nums`,我们需要找到其中的一个子数组,通过计算这个子数组所有元素的乘积,得到的...

    乘积最大的拆分.zip

    这个问题的关键在于理解如何有效地搜索和比较不同的子序列组合,以确定哪个乘积最大。在处理这个问题时,可能会用到动态规划、回溯、贪心算法或者分治策略等常见的算法思想。 首先,我们可以考虑一个简单的暴力求解...

    算法-乘积最大(信息学奥赛一本通-T1275)(包含源程序).rar

    这可能是一个单向的乘积最大值问题,也可能是要求连续子序列的最大乘积。 2. **算法设计**: - **暴力方法**:最直观的方法是枚举所有可能的子序列,计算其乘积并比较,但这种方法的时间复杂度是O(n^2),在大数据...

    乘积最大子数组(java代码).docx

    在处理一系列数值时,经常会遇到求解特定子序列(或子数组)的问题。对于“乘积最大子数组”这一问题,其核心是寻找一个连续子数组,使得该子数组中所有元素的乘积最大。 #### 二、问题描述 给定一个整数数组 `...

    java代码-LeetCode 152. 乘积最大子序列

    在LeetCode的第152题“乘积最大子序列”中,我们面临的挑战是找到一个整数数组中乘积最大的连续子序列。这个题目属于动态规划和数组处理的范畴,是Java编程中常见的算法问题。下面将详细介绍解决这个问题的关键知识...

    蓝桥杯c++-蓝桥杯竞赛练习之算法提高题最大乘积.zip

    描述中提到的“蓝桥杯c++_蓝桥杯竞赛练习之算法提高题最大乘积”进一步确认了这是一个与蓝桥杯竞赛相关的C++编程练习,特别关注算法的提升和优化,目标是解决找到数组中连续子数组的最大乘积的问题。这个问题在实际...

    动态规划实例解析及C++代码实现

    - **最大连续子序列积**:与最大连续子序列和类似,但寻找的是最大乘积而非和。 - **正整数的无序分拆**:给定一个正整数,找出所有可能的由正整数之和构成的分拆,可以使用动态规划记录每个数的分拆方案。 以上...

    算法设计与分析作业及答案

    - **最大子长方体**:可能是指在一维数组中寻找连续子数组的最大乘积,这通常用到双指针或者动态规划解决。 - **二路合并**:是归并排序的一部分,它将两个已排序的序列合并成一个有序序列,使用了分治思想和两个...

    2553331_ssa_SSA去噪_SSA算法_时间序列_奇异谱_奇异谱分析

    **奇异谱分析(Singular Spectrum Analysis, SSA)**是一种用于数据降噪和信号提取的统计方法,特别适用于处理时间序列数据。它结合了线性代数、谱分析和时间序列建模,旨在揭示隐藏在复杂数据背后的结构和周期性...

    计算机算法设计与分析3

    - **最大子段和**:在数组中找到连续子数组的最大和。 - **凸多边形最优三角剖分**:找到分割多边形为最少三角形的方案。 - **多边形游戏**:涉及策略和几何形状的游戏问题。 - **图像压缩**:使用动态规划优化图像...

    计算机算法设计的复习资料

    这个问题的目标是从一组整数中找到连续子序列的最大和。例如,在序列(-2, 11, -4, 13, -5, -2)中,最大子段和为20。 ##### 简单蛮力算法 - **实现**:遍历数组中的每一个元素,并计算以该元素开头的所有子序列的和...

    数学分析变态难题 .pdf

    20. 证明任何实数列都有单调子列,这涉及到实数的顺序性质和子序列的构造。 21. 证明严格单调增序列存在两两互素的子列,涉及到数论和序列的性质。 22. 分析函数fn.x/的收敛性,这需要对函数的性质有深入理解,并...

    实分析(Royden)习题全解

    - **拓扑空间的乘积与直积**:分析拓扑空间乘积和直积的构造与性质。 - **拓扑与一致性质**:比较拓扑空间与一致空间之间的联系与区别。 - **网**:介绍网的概念,以及其在拓扑空间中的应用。 ### 紧致空间 - **...

    泛函分析课件

    泛函分析是数学的一个重要分支,它主要研究函数空间的结构、性质以及这些空间上的算子理论。...学习过程中,建议结合实际例子和问题来加深理解,同时,练习证明定理和解决相关问题,以巩固理论知识。

    《算法分析与设计试卷2016-2017》.doc

    - **最大子段和问题**: 同样可以通过动态规划法来求解。这类问题的目标是找到连续子数组中的最大和。 - **Strassen矩阵乘法**: 利用分治策略实现的一种快速矩阵乘法算法。它可以显著减少矩阵乘法所需的计算量。 ###...

    算法-数列分段Section I(洛谷-P1181).rar

    例如,可能需要找出数列中最长的连续子序列,使得子序列的所有元素之和不超过一个给定的阈值,或者找到满足特定条件的最大子序列数量。 解这类问题时,我们需要理解并熟练运用以下知识点: 1. **数组和链表**:...

Global site tag (gtag.js) - Google Analytics