题意:给你一段序列,在这个序列中找出m个子段使这m个子段的和最大。
分析:
从题意的理解中我们可以看出影响这个问题的规模有两个,一个是这个序列的长度,另一个是这个序列分成的子段个数。所以我们要从这两个影响问题规模的因素上考虑俩缩小问题的规模。求i个数分成m个子段的最大和可以把它转化为一下几个问题:
(1)求i-1个数分成m个子段的最大和。
(2)求i-1个数分成m-1个子段的最大和。
(3)求i-1个数分成m个子段的最大和并且第m个子段是以第i-1个数结尾的。
和我写的一篇关于如何求最大子段和的文章中分析的相似,原问题和问题(3)不是同构的,我们在设计动态规划的动态转移方程的时候总是希望原问题与分解所产生的子问题是同构的,这样有利于我们利用子问题的解构造出原问题的解。
我们尝试着把原问题转化为求i个数分成m个子段并且第m个子段是以第i个数结尾的,这样得到的子问题是以下的子问题:
(1)求i-1个数分成m个子段并且第m个子段是以第i-1个数结尾的最大和。
(2)求k个数(k >= m-1 && k <= i-1)分成m-1个子段并且第m-1个子段是以第k个数结尾的。
这样原问题与子问题就是同构的。并且原问题与子问题之间满足最优子结构的特征和无后效性的性质。
动态转移方程是:dp[i][j] = max(dp[i-1][j], dp[k][j-1])+a[i],其中k >= j-1 && k < i; dp[i][j]表示前i个数中以第i个数结尾分成j段的最大和。
因为i和j都是比较大的,所以我们使用滚动数组来优化空间复杂度;
为了说清楚滚动数组,我们先来看看动态转移方程。在动态转移方程中,求解j段的最大和只与j段的最大和 和 j-1段的最大和有关。所以我们使用两个一维数组dp1[maxn],dp2[maxn];在求解的过程中只要动态的更新表示j-1段最大和的数组即可。即:
当j == 2时,dp2[i]表示的是分成一段的最大和。
当j == 5时,dp2[i]表示的是分成4段的最大和。
然后当使用完时一定要记得更新,以便于下一次的使用。
另外还有一个时间按上的优化,如何得到dp[k][j-1]的最大值呢?我们可以在求dp[i][j]的最大值时顺便得到一下这个最大值。
下面是代码:
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
using namespace std;
const int maxn = 1000000+10000, inf = 0x7fffffff;
int a[maxn], dp[maxn][2];
int main()
{
int n = 0, m = 0;
while(scanf("%d%d",&m,&n) != EOF)
{
for(int i = 1; i <= n; i++)
{
scanf("%d",&a[i]);
}
memset(dp,0,sizeof(dp));
for(int j = 1; j <= m; j++)
{
int tmp = -inf;
dp[j-1][1] = -inf;
for(int i = j; i <= n; i++)
{
if(tmp < dp[i-1][0])tmp = dp[i-1][0]; //tmp 表示的就是dp[k][j-1]的最大值; j-1=<k<= i-1;
dp[i][1] = max(dp[i-1][1],tmp)+a[i];
}
for(int i = j; i <= n; i++)
{
dp[i][0] = dp[i][1];
}
}
int ans = -inf;
for(int i = m; i <= n; i++)
{
ans = max(ans,dp[i][1]);
}
printf("%d\n",ans);
}
return 0;
}