【数据结构】最大子序列问题

已知序列A ={a1,a2,a3, ... ,aN}求序列A的最大子序列,已知序列A中均为整数,如果全为负数,直接返回0

下面使用不同复杂度的算法进行实现:

1.复杂度为O(N^3)

算法使用三层for循环完成,因为比较容易理解,不再赘述,直接写出程序,代码如下所示:

//使用复杂度为O(N^3)的算法实现最大子序列问题
#include <vector>
#include <iostream>

using namespace std;

int MaxSubsequenSum(vector<int>A);

int main()
{
    int maxSubSum = 0;
    vector<int> A = {-2,11,-4,13,-5,-2};
    maxSubSum = MaxSubsequenSum(A);
    cout<<"最大子序列的和是:"<<maxSubSum<<endl;
    return 0;
}

int MaxSubsequenSum(vector<int>A)
{
    int size = A.size();
    int MaxSum = 0;
    int flag = 0;
    int AllSum = 0;
    for(int i = 0;i < size;i++)
    {
        if(A.at(i) > 0) {
            flag += 1;
            AllSum += A.at(i);
        }
    }
    if(flag == 0)   //全为负数,则返回0
        return 0;
    if(flag == size)   //全为正数,则返回全部之和
    {
       return AllSum;
    }
    //下面讨论数组中有正数也有负数的情况

    for(int m = 0;m < size;m++)
    {
        //第一层循环表示从第m个数开始加
        for(int n = m;n < size;n++)
        {
            //第二层循环加到n
            int ThisSum = 0;
            for(int p = m;p <= n;p++)
            {
                //第三层表示一直加到第n个的过程
                ThisSum += A[p];
                if(ThisSum > MaxSum)
                    MaxSum = ThisSum;
            }
        }
    }
    return MaxSum;
}

2.算法复杂度为O(N^2)

将第三层循环去除,直接使用两层循环解决即可。

具体程序如下所示:

//使用复杂度为O(N^2)的算法实现最大子序列问题
#include <vector>
#include <iostream>

using namespace std;

int MaxSubsequenSum(vector<int>A);

int main()
{
    int maxSubSum = 0;
    vector<int> A = {-2,11,-4,13,-5,-2};
    maxSubSum = MaxSubsequenSum(A);
    cout<<"最大子序列的和是:"<<maxSubSum<<endl;
    return 0;
}

int MaxSubsequenSum(vector<int>A)
{
    int size = A.size();
    int MaxSum = 0;
    int flag = 0;
    int AllSum = 0;
    for(int i = 0;i < size;i++)
    {
        if(A.at(i) > 0) {
            flag += 1;
            AllSum += A.at(i);
        }
    }
    if(flag == 0)   //全为负数,则返回0
        return 0;
    if(flag == size)   //全为正数,则返回全部之和
    {
       return AllSum;
    }
    //下面讨论数组中有正数也有负数的情况

    for(int m = 0;m < size;m++)
    {
        //第一层循环表示从第m个数开始加
        int ThisSum = 0;
        for(int n = m;n < size;n++)
        {
            //第二层循环表示从n开始加
            ThisSum += A[n];
            if(ThisSum > MaxSum)
                MaxSum = ThisSum;
        }
    }
    return MaxSum;
}


3.复杂度为O(N logN)

这种算法使用递归的思想,采用的是一种“分治”的策略,主要想法是把问题分解成两个大致相等的子问题,然后递归地对他们进行求解,这是“分”的部分,“治”的阶段是将两个子问题的解合并到一起并可能再做些少量的附加工作,最后得到整个问题的解。

在这个问题中,要求的最大子序列可能在三处出现,要么是整个出现在输入数据的左半部分,要么是整个出现在右半部分,或者是跨越输入数据的中部从而占据左右两半部分。前两种情况可以进行递归求解。第三种情况的最大和可以通过求出前半部分的最大和(包含前半部分的最后一个元素)及后半部分的最大和(包含后半部分的第一个元素)而得到。然后将这两个和加在一起。

下面先写出程序,再对程序进行说明。

/**使用复杂度为O(N·logN)的算法实现最大子序列问题**/
#include <vector>
#include <iostream>

using namespace std;

int maxSumRec(const vector<int> &a,int left,int right);
int maxSubSum3(const vector<int> & a);

template <typename  T>
T max3(T a,T b,T c);

int main()
{

    int maxSubSum = 0;
    vector<int> A = {-2,11,-4,13,-5,-2};
    maxSubSum = maxSubSum3(A);
    cout<<"最大子序列的和是:"<<maxSubSum<<endl;
    return 0;
}

/**
 * 相连最大子序列和的递归算法
 * 找出生成[left...Right]的子数组中的最大和
 * 不试图保留具体的最佳序列
 */
int maxSumRec(const vector<int> &a,int left,int right)
{
    if(left == right)
    {
        if(a[left] > 0)
            return a[left];
        else
            return 0;
    }

    /**递归计算左右半边的最大子序列的和**/
    int Center = (left + right) / 2;
    int maxLeftSum = maxSumRec(a, left, Center);
    int maxRightSum = maxSumRec(a, Center+1, right);

    /**计算左边(包括Center)的最大值**/
    int maxLeftBorderSum = 0, leftBorderSum = 0;
    for(int i = Center;i >= left;--i)
    {
        leftBorderSum += a.at(i);
        if(leftBorderSum > maxLeftBorderSum)
            maxLeftBorderSum = leftBorderSum;
    }

    /**计算右边(不包括Center)的最大值**/
    int maxRightBorderSum = 0, rightBorderSum = 0;
    for(int j = Center + 1;j <= right; j++)
    {
        rightBorderSum += a.at(j);
        if(rightBorderSum > maxRightBorderSum)
            maxRightBorderSum = rightBorderSum;
    }
    return max3(maxLeftSum, maxRightSum,maxLeftBorderSum + maxRightBorderSum);


}

/**
 * 相连最大子序列和分治算法的驱动程序
 */
int maxSubSum3(const vector<int> & a)
{
    return maxSumRec(a, 0, a.size() - 1);
}

/**选出三个数中的最大值**/
template <typename  T>
T max3(T a,T b,T c)
{
    T d = a>b ? a:b;
    return (d>c ? d:c);
}

说明:

上面程序中递归函数调用的一般形式是传递输入的数组以及左边界和右边界,它们界定了数组的处理范围,单行的驱动程序通过传递数组以及边界0和N-1从而将maxSumRec函数启动。

在处理基准情况的过程中,如果left == right,那么说明只有一个元素,并且当该元素非负时它就是最大的子序列。下面执行的两个递归调用,可以看出来,递归调用总是对小于原问题的问题进行,不过程序中小的扰动可能会破坏这个特性。接下来的程序用来计算达到中间分界处的两个最大和的和数。这两个值为拓展到左右两部分的最大和。函数max3表示返回这三个可能最大和中的最大者。


4.复杂度为O(N)

先给出具体程序如下所示:

/**
 * 线性时间最大相连子序列和算法
 * **/
#include <vector>
#include <iostream>

using namespace std;

int maxSubSum4(const vector<int> & a);

int main()
{
    int maxSubSum = 0;
    vector<int> A = {-2,11,-4,13,-5,-2};
    maxSubSum = maxSubSum4(A);
    cout<<"最大子序列的和是:"<<maxSubSum<<endl;
    return 0;
}

int maxSubSum4(const vector<int> & a)
{
    int maxSum = 0,thisSum = 0;
    for(int j = 0;j < a.size(); j++)
    {
        thisSum += a[j];    //向右累加

        if(thisSum > maxSum)    
            maxSum = thisSum;   /**发现更大的和就更新当前的结果**/
        else if(thisSum < 0)    /**如果当前子列和为负**/
            thisSum = 0;        /**则不可能使得后面的部分和更大,直接抛弃处理**/
    }
    return maxSum;
}



本程序像算法1和算法2一样,j代表当前序列的终点,而i代表当前序列的起点。需要注意的是,如果我们不需要知道具体最佳的子序列在哪里,那么i的使用可以从程序上被优化掉,不过在设计算法的时候还是假设i是需要的,而且想要通过改进算法2得到算法4.

一个重要的结论是,如果a[i]是负的,那么它不可能代表最优序列的起点,因为任何包含a[i]作为起点的序列都可以通过用a[i+1]做起点而得到改进。类似的,任何负的子序列是不可能是最优子序列的前缀(原理相同)。如果在内循环中我们检测到从a[i]到a[j]的子序列是负的,那么可以推进i。

关键结论是:我们不仅能够把i推进到i+1,而且实际上还可以把它一直推进到j+1,为了说明这一点,我们令p为i+1到j的任意一个下标,开始于下标p的任意子序列都不大于在下标i开始并包含从a[i]到a[p-1]的子序列的对应的最序列,因为后面这个子序列不是负的(j是是的从下标i开始其值成为负值序列的第一个下标)。因此,把i推进到j+1是没有任何风险的,这样也不会错过最优解

此外,这个算法附带的一个优点是,它只对数据进行一次扫描,一旦a[i]被读入并被处理,它就不再需要被记忆。这是一种在线算法,也叫做联机算法(on-line algorithm)。

“在线”的意思是指每输入一个数据就能进行即时处理,在任何一个地方终止输入,算法都能正确给出当前的解。

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


至此,最大子序列的算法介绍完毕。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值