题目网址:Max Sum Plus Plus
思路:dp[i][j]表示前j项分为i个子段且j属于最后一个子段的答案。dp[i][j]能由哪些状态转移过来呢 ?
①dp[i][j-1]+num[j],表示我们把第j个元素加到最后一段中去。
②max(dp[i-1][k])+num[j](0<k<j), 表示我们让第j个元素成为新的一段。
初始化所有状态都为0。
动态转移方程:dp[i][j]=max(dp[i][j-1]+num[j],max(dp[i-1][k])+num[j]),其中0<k<j
我们发现需要3重for循环,m*(n^2)会超时,m*n空间会炸,我们不妨先写一下看看哪里能优化。
超时代码(且空间开的小)!!!:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e3+5;//实际上是1e6
const int INF = 0x3f3f3f3f;
int m, n, a[MAXN], pre[MAXN], dp[105][MAXN];
int main()
{
while (~scanf("%d%d", &m, &n))
{
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
memset(dp, 0, sizeof(dp));
memset(pre, 0, sizeof(pre));
int ans = -INF;
for (int i = 1; i <= m; i++)
{
for (int j = i; j <= n; j++)
{
int MAX = -INF;
for (int k = 1; k < j; k++)
{
MAX = max(MAX, dp[i-1][k]);
}
dp[i][j] = max(dp[i][j-1]+a[j], MAX+a[j]);
ans = max(ans, dp[i][j]);
}
}
printf("%d\n", ans);
}
return 0;
}
/*
1 3
1 2 3
2 6
-1 4 -2 3 -2 3
*/
我们可以发现其实最内层k的for循环,起到的作用并不大,只是求得一个max(dp[i-1][k])(0<k<j),而且 j 每增加1,k 就会重新for一遍,做了很多无用功,相比 j-1 的时候只差一个与dp[i][j-1]的比较,所以我们用一个变量MAX维护max(dp[i-1][k])(0<k<j)这个值就OK了。
此时,我们只需要2重for即可,时间复杂度为n^2,但是空间依旧会炸,我们还把这份代码先写下来,再继续优化。
空间炸代码!!!(也会超时,因为初始化原因,但时间复杂度比上面降低了):
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e3+5;//实际上是1e6
const int INF = 0x3f3f3f3f;
int m, n, a[MAXN], pre[MAXN], dp[105][MAXN];
int main()
{
while (~scanf("%d%d", &m, &n))
{
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
memset(dp, 0, sizeof(dp));
memset(pre, 0, sizeof(pre));
int ans = -INF;
for (int i = 1; i <= m; i++)
{
int MAX = -INF;
for (int j = i; j <= n; j++)
{
MAX = max(MAX, dp[i-1][j-1]);
dp[i][j] = max(dp[i][j-1]+a[j], MAX+a[j]);
ans = max(ans, dp[i][j]);
}
}
printf("%d\n", ans);
}
return 0;
}
/*
1 3
1 2 3
2 6
-1 4 -2 3 -2 3
*/
我们继续考虑如何优化空间,我们能发现dp[i]的状态只和dp[i-1]的状态有关,我们是不是能用到滚动数组呢?当然可以。
用dp[j]表示前 j 个数字分为 i 段的答案,我们把第一维省略了。因为我们要用到dp[i-1]的状态,所以我们另外开一个另外的数组pre记录dp[i-1]的状态(pre[j]表示dp[i-1][k](0<k≤j)的最大值)。因为每次dp[j](即原来的dp[i][j])的状态需要用到pre[j-1](原来的dp[i-1][j-1])的状态,所以每次dp[j]计算结束,我们不能立刻更新pre[j](因为下一次循环j+1的时候我们要用到pre[j](即原来的dp[i-1][j]),我们如果直接更新了pre[j],就会影响j+1状态的转移),我们先用Max记录pre[j](也就是max(Max,dp[j])),然后等到j+1的状态计算完毕,再更新pre[j]=Max,在维护新的pre[j]的值Max。
此时状态转移方程为:dp[j]=max(dp[j]+num[j],pre[j-1]+num[j])
且可以发现最终答案就是Max,所以我们直接输出Max就好。
此时,时间复杂度为n*m,空间复杂度为n,就OK了。
可以发现pre数组+Max的更新很巧妙,感觉像是用Max延迟了一下pre的更新。
pre数组中一直是左右两部分不同含义的值,假设当前循环为 i 段的情况,左边部分份为 i 段的状态(即i+1的时候的pre),右边部分为 i-1 段的状态,用于更新dp[j]。
卡了好长时间的一道题,大家理解理解。
AC代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e6+5;
const int INF = 0x3f3f3f3f;
int m, n, a[MAXN], pre[MAXN], dp[MAXN];
int main()
{
while (~scanf("%d%d", &m, &n))
{
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
memset(dp, 0, sizeof(dp));
memset(pre, 0, sizeof(pre));
int Max;
for (int i = 1; i <= m; i++)
{
Max = -INF;
for (int j = i; j <= n; j++)
{
dp[j] = max(dp[j-1]+a[j], pre[j-1]+a[j]);
pre[j-1] = Max;
Max = max(Max, dp[j]);
}
}
printf("%d\n", Max);
}
return 0;
}
/*
1 3
1 2 3
2 6
-1 4 -2 3 -2 3
*/