HDU ~ 1024 ~ Max Sum Plus Plus(DP+滚动数组)

题目网址:Max Sum Plus Plus


题意:每一行前两个数m(0<m<1e2),n(0<n<1e6)表示要从这n个数字中挑m个子段,要求这些子段不相交,求最大这m段的最大和。

思路: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<kj)的最大值)。因为每次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
*/




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值