C++实现动态规划法解决最大m子段和问题

问题描述

给定由n个整数(可能为负整数)组成的序列a1,a2,…,an和正整数m,要求确定序列a1,a2,…,an的m个不相交子段,使这m个子段的总和达到最大。
最大m子段和问题是最大子段和问题在子段个数数目的推广。换句话说,最大子段和问题是最大m子段和问题当m=1时的特殊情况。关于m=1的最长子段和问题,可以参考我的另一篇文章:四种方法求最长子段和

题目分析

设b(i,j)表示数组a的前j项中i个子段和的最大值,且第i个子段含a[j] (1<=i<=m,i<=j<=n),则所求的最优值显然为max b(m,j)。与最大子段和问题类似,计算b(i,j)的递归式为:

b(i,j) = max{b(i,j-1)+a[j],maxb(i-1,t) +  a[j]} (1<=i<=m,i <= j <= n,i - 1<=t <=j)

其中b(i,j-1)+a[j]项表示第i个子段含a[j-1],而max b(i-1,t) +a[j]项表示第i个子段仅含a[j]。初始化为b(0,j)=0,b(i,0)=0.

普通方法代码

#include <iostream>
using namespace std;
int main()
{
    int n,m;
    cin>>n>>m;
    if(n < m || m < 1)
    {
        cout<<0<<endl;
        return 0;
    }
    int a[n+1];
    for(int i = 1;i <= n;i++)
        cin>>a[i];
    int **b = new int *[m+1];
    for(int i = 0;i <= m;i++)
        b[i] = new int [n+1];
    for(int i = 0;i <= m;i++)
        b[i][0] = 0;
    for(int i = 1;i <= n;i++)
        b[0][i] = 0;
    for(int i = 1;i <= m;i++)
    {
        for(int j = i;j <= n - m + i;j++)
        {
            if(j > i)
            {
                b[i][j] = b[i][j-1]+a[j];
            }
            else
                b[i][j] = b[i-1][j-1]+a[j];//如果j==i,我们就将a[j]单独作为一项,作为第i个子段
            for(int k = i-1;k < j;k++)
            {
                if(b[i][j]<b[i-1][k] + a[j])
                //寻找中间某个k作为第i-1个子项的结尾,且这个k使得第i-1个子段和最大,然后我们将a[j]作为第i个子段和,进行更新比较。
                    b[i][j] = b[i-1][k] + a[j];
            }
        }
    }
    int sum = 0;
    for(int j = m;j <= n;j++)
    {
        if(sum < b[m][j])
            sum = b[m][j];
    }
    cout<<sum;
    return 0;
}

时间复杂度O(mn^2),最外层循环执行m次,内层两次循环都执行n的相关次。
空间复杂度O(mn)

优化方法代码

通过上一段代码我们可以看出,我们其实只用了b的前一行和当前行,即b[i-]和b[i],所以其他的行是多余的,而且会造成空间上的浪费。我们在计算第i-1行时将b[i-1][t]保存下来,这样在计算b[i-1][t]+a[j]时不需要重新计算,从而节约了时间。
因此我们只需要创建两个一维数组,一个是m维的c数组,负责存储上一个子段,即i-1子段和的最大值;一个是n维的b数组,负责存储每个数值对应的m最大子段和值。
具体看代码

#include <iostream>
using namespace std;
int MaxSum(int m,int n,int *a)
{
    if(n < m || M < 1)
        return 0;
    int *b = new int [n+1];
    int *c = new int [n+1];//由于我们最原始的b是个二维矩阵,我们在更新值的时候只看当前行和前两行
    //所以我们只需要用两个数组就可以,一个当前状态最大值,另一个存上一个状态值(即二维数组中的上一行最大值)
    b[0] = 0;
    c[1] = 0;//c[1]的上一行的最大值即是b[0]的值,即c[i-1]=b[i]
    for(int i = 1;i <= m;i++)
    {
        b[i] = b[i-1] + a[j];//初始化为a[j]单独作为一项
        c[i-1] = b[i];
        int max = b[i];
        for(int j = i+1;j < n + i - m;i++)//因为我们一共有m个子段,当前占用了i段
        //极端情况为一个子段为一个字符,所以此时最多还剩n-m+i个字符
        {
            b[j] = b[j-1] > c[j-1] ? b[j-1]+a[j] : c[j-1]+a[j];//这两种情况分别是我们已经到了第i个子段,结尾现在是a[j-1]
            //我们要将a[j]加进去,将它与c[j-1]+a[j]比较,c[j-1]+a[j]表示的是目前只有n-1个子段和,我们将a[j]单独设为第i段
            c[j-1] = max;//更新第i-1个子段和的最大值
        }
        c[i+n-m] = max;//最后一次更新的上一个子段
    }
    int sum = 0;
    for(int i = m;i <= n;i++)
    {
        if(sum < b[i])
            sum = b[i];
    }
    return sum;
    return 0;
}

时间复杂度O(m(n-m)),外层循环执行m次,而内层循环执行n-m次。因为我们找的过渡段是从i-1子段和到i子段和,所以此时j=i+1,而由于有i个子段和了,所以后面的元素最多遍历到n-m+i的位置,此时执行次数为n-m+i-i-1 = n-m-1,记为n-m次。
空间复杂度O(n)

发布了60 篇原创文章 · 获赞 2 · 访问量 1057

猜你喜欢

转载自blog.csdn.net/weixin_44755413/article/details/105590179