Helvetic Coding Contest 2018 C2(经典DP)

Encryption (medium)

Heidi has now broken the first level of encryption of the Death Star plans, and is staring at the screen presenting her with the description of the next code she has to enter. It looks surprisingly similar to the first one – seems like the Empire engineers were quite lazy…

Heidi is once again given a sequence A, but now she is also given two integers k and p. She needs to find out what the encryption key S is.

Let X be a sequence of integers, and p a positive integer. We define the score of X to be the sum of the elements of X modulo p.

Heidi is given a sequence A that consists of N integers, and also given integers k and p. Her goal is to split A into k part such that:

  • Each part contains at least 1 element of A, and each part consists of contiguous elements of A.
  • No two parts overlap.
  • The total sum S of the scores of those parts is maximized.

Output the sum S – the encryption code.

Input

The first line of the input contains three space-separated integer N, k and p (k ≤ N ≤ 20 000, 2 ≤ k ≤ 50, 2 ≤ p ≤ 100) – the number of elements in A, the number of parts A should be split into, and the modulo for computing scores, respectively.

The second line contains N space-separated integers that are the elements of A. Each integer is from the interval [1, 1 000 000].

Output

Output the number S as described in the problem statement.

Examples

Copy

4 3 10
3 4 7 2

Copy

16

Copy

10 5 12
16 3 24 13 9 8 7 5 12 12

Copy

37

Note

In the first example, if the input sequence is split as (3, 4), (7), (2), the total score would be img. It is easy to see that this score is maximum.

In the second example, one possible way to obtain score 37 is to make the following split: (16, 3, 24), (13, 9), (8), (7), (5, 12, 12).

题意

将数分成k段,要使每一段和%p的和最大

输出最大结果

k ≤ N ≤ 20 000, 2 ≤ k ≤ 50, 2 ≤ p ≤ 100

一开始我的思路(记忆化搜索)

一开始我认为复杂度是 O(n2) O ( n 2 ) 也许可以蹭过去

提交后 TLE on test 26 T L E   o n   t e s t   26

仔细分析复杂度为 O(n2k) O ( n 2 ∗ k )

为什么是 O(n2k) O ( n 2 ∗ k ) 呢?

开始传参,dfs(0,k) 那会递归求解dfs(1,k-1),dfs(2,k-1),…,dfs(n,k-1)

接着再递归,直到第二维 j=1 j = 1

这样看上去复杂度是 O(nk) O ( n k )

但是由于是记忆化的,有 nk n ∗ k 个状态

但是求得这些状态的复杂度并不是 O(nk) O ( n ∗ k ) 的,而是 O(nnk) O ( n ∗ n ∗ k )

即一个状态会被访问 O(n) O ( n ) 级别次,比如 dp[n100][k2] d p [ n − 100 ] [ k − 2 ] 会被 dp[0][k1] d p [ 0 ] [ k − 1 ] 访问,也会被 dp[1][k1],dp[2][k1].... d p [ 1 ] [ k − 1 ] , d p [ 2 ] [ k − 1 ] . . . . 访问

总的时间复杂度是 O(nnk) O ( n ∗ n ∗ k )

img

考虑进一步优化,见下文

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int debug_num=0;
#define debug cout<<"debug "<<++debug_num<<" :"

const int maxn=2e4+10;
const int inf=1e9;

int a[maxn];
int pre[maxn];
int dp[maxn][51];
int n,k,p;

int dfs(int i,int j)
//i,j表示从i+1位到n位分j段的最大值s
{
    if(j==1){
        if(dp[i][j]==-1) return dp[i][j]=(pre[n]-pre[i])%p;
        else return dp[i][j];
    }
    if(j==n-i){//一个简单的优化
        if(dp[i][j]==-1) return dp[i][j]=(pre[n]-pre[i]);//不用模p
        else return dp[i][j];
    }
    int maxx=0;
    for(int l=i+1;l<=n;++l)//枚举下一次从哪分得到的价值最大
    {
        int &ret=dp[l][j-1];
        if(ret==-1){
            if(j-1>n-l) ret=-inf;//显然不能够分这么多段
            else ret=dfs(l,j-1);
            maxx=max(maxx,(pre[l]-pre[i])%p+ret);
        }
        else maxx=max(maxx,(pre[l]-pre[i])%p+ret);
    }
    return dp[i][j]=max(dp[i][j],maxx);
}

int main()
{
    //freopen("in.txt","r",stdin);
    ios::sync_with_stdio(false);
    cin>>n>>k>>p;
    for(int i=1;i<=n;++i){
        cin>>a[i];
    }
    for(int i=1;i<=n;++i){
        pre[i]=pre[i-1]+a[i]%p;
        //cout<<"pre:"<<i<<": "<<pre[i]<<"    ";
    }
    //cout<<endl;
    for(int i=0;i<=n;++i){
        for(int j=0;j<=k;++j){
            dp[i][j]=-1;
        }
    }
    dfs(0,k);
//    for(int i=0;i<=n;++i){
//        for(int j=)
//    }
    cout<<dp[0][k]<<endl;
    return 0;
}

优化后的记忆化搜索

如果想要降低记忆化搜索的时间复杂度,dfs内枚举n不太行了

注意题目中p只有100

可以可以利用这个限制用空间换时间呢?

算一下 npk n ∗ p ∗ k 还在512MB内…(当然CF的空间还是很良心的,别的OJ就不一定了)

我们不去枚举下一次在哪分段最优,而是直接做”两路”选择,即当前位下一位在一段当前位和下一位不在一段 (意味着当前位是最后一位),但问题是我们不知道具体是怎么分段得到的结果最优。利用每一段的和取模p后的值在100内,我们可以再带个参数,即下面代码中的rem,表示当前段(不包括当前位)的和%p的值。而对于两种决策,若下一位是新段的开始(j-1),即当前位一定是该段的结尾,加到rem上;若下一位和当前位还是同一段,则rem加上当前位的值。(下一位的值先不加,早晚会加)

边界条件 在上面的约定下,边界条件就是当位置i来到n-1时(从0编号),则只有选择当前位是该段的结尾才有意义,因为后面没数了,即ret=max(ret,(rem+a[i])%p+dfs(i+1,j-1,0));

(rem+a[i])%p ( r e m + a [ i ] ) % p 更新了该段的新值

时间复杂度 O(nkp) O ( n ∗ k ∗ p )

空间复杂度 O(nkp) O ( n ∗ k ∗ p )

img

time(limit=3s)和Memory都很恐怖

递归次数太多了,时间常数变大了

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int debug_num=0;
#define debug cout<<"debug "<<++debug_num<<" :"

const int maxn=2e4+10;
const int inf=1e9;

int a[maxn];
int dp[maxn][51][100];
int n,k,p;

int dfs(int i,int j,int rem)
{
    if(i==n){
        if(j>0) return -inf;
        return 0;
    }
    if(j<=0) return -inf;
    int &ret=dp[i][j][rem];
    if(ret!=-1) return ret;
    if(n-i<j) return ret=-inf;
    ret=dfs(i+1,j,(rem+a[i])%p);
    ret=max(ret,(rem+a[i])%p+dfs(i+1,j-1,0));
    return ret;
}

int main()
{
#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
#endif // ONLINE_JUDGE
    ios::sync_with_stdio(false);
    cin>>n>>k>>p;
    for(int i=0;i<n;++i) cin>>a[i];
    memset(dp,-1,sizeof(dp));
    //从0开始一共分k段 很自然
    //a[0]一定是第一段的开始,但不是结束,所以可以将第一维理解成前一段目前结尾的数 而rem就是当前这一段的和%p的值
    cout<<dfs(0,k,0)<<endl;//即初始的rem值
    return 0;
}

一般记忆化搜索可以写成递推式,而不是递归式子。

写递归的式子,可能有助于你对问题的思考,因为从大往小的递归比较自然,再加上记忆化,复杂度可以保证。

但是递归传参需要耗费一定时间

下面考虑用递推的方法求解本题,看能否在空间和效率上得到优化

DP1(朴素版本的DP)

类似上面第一个记忆化,可以直接写出一个 O(n2k) O ( n 2 k ) 的递推式

dp[i][j] d p [ i ] [ j ] 表示,前i个数,分成j段的最大值

转移式子:dp[i][j]=max(dp[i][j],dp[l][j-1]+(pre[i]-pre[l])%p);

当然,会超时, 但空间上小了很多啦

img

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int debug_num=0;
#define debug cout<<"debug "<<++debug_num<<" :"
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
const int inf = 0x3f3f3f3f;
const ll inff = 0x3f3f3f3f3f3f3f3f;

const int maxn=2e4+10;
int a[maxn];
int n,k,p;
int dp[maxn][51];
int pre[maxn];

int main()
{
#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
#endif
    ios::sync_with_stdio(false);
    cin>>n>>k>>p;
    for(int i=1;i<=n;++i){
        cin>>a[i];
        pre[i]=pre[i-1]+a[i];
    }


    for(int i=0;i<=n;++i) dp[i][0]=-inf;
    dp[0][0]=0;
    for(int i=1;i<=n;++i){
        for(int j=1;j<=k;++j){//分j段
            if(i<j) {
                dp[i][j]=-inf;
                continue;
            }
            for(int l=0;l<i;++l){
                dp[i][j]=max(dp[i][j],dp[l][j-1]+(pre[i]-pre[l])%p);
            }
        }
    }

    cout<<dp[n][k]<<endl;
    return 0;
}

DP2(三维版本的DP)

显然,这种方法想要用空间换时间,思路类似优化的记忆化搜索

具体见代码

但是速度快了很多(没有了参数的传递)

img

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int debug_num=0;
#define debug cout<<"debug "<<++debug_num<<" :"
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
const int inf = 0x3f3f3f3f;
const ll inff = 0x3f3f3f3f3f3f3f3f;

const int maxn=2e4+10;
int a[maxn];
int n,k,p;
int dp[maxn][51][100];
int ans[maxn][51];

int main()
{
#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
#endif
    ios::sync_with_stdio(false);
    cin>>n>>k>>p;
    for(int i=1;i<=n;++i){
        cin>>a[i];
        a[i]%=p;
    }
    //i:前i个点   j:分段数   l:前面一段的和模值
    int tp=0;
    for(int i=1;i<=n;++i){//对分一段的情况初始化
        tp+=a[i]; tp%=p;
        dp[i][1][tp]=tp;
        ans[i][1]=tp;
    }
    for(int i=1;i<=n;++i){
        for(int j=2;j<=k&&j<=i;++j){//从分两段开始
            for(int l=0;l<p;++l){
                tp=(l-a[i]+p)%p;//tp+a[i]=l 对当前模的结果l而言  其由tp+a[i]得到 即a[i]和前一段模值是DP的前面所有段的和
                dp[i][j][l]=dp[i-1][j][tp]-tp+l;//模数为l
                if(a[i]==l)//单独分组
                    dp[i][j][l]=max(dp[i][j][l],ans[i-1][j-1]+a[i]);
                ans[i][j]=max(ans[i][j],dp[i][j][l]);
            }
        }
    }
    cout<<ans[n][k]<<endl;
    return 0;
}

DP3(时空优化的DP)

时间可观了,但是空间消耗太大,将数组滚动起来,可以节省巨大的空间

img

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int debug_num=0;
#define debug cout<<"debug "<<++debug_num<<" :"
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
const int inf = 0x3f3f3f3f;
const ll inff = 0x3f3f3f3f3f3f3f3f;

const int maxn=2e4+10;
int a[maxn];
int n,k,p;
int dp[2][51][100];
int ans[maxn][51];

int main()
{
#ifndef ONLINE_JUDGE
    freopen("in.txt","r",stdin);
#endif
    ios::sync_with_stdio(false);
    cin>>n>>k>>p;
    for(int i=1;i<=n;++i){
        cin>>a[i];
        a[i]%=p;
    }
    //i:前i个点   j:分段数   l:当前一段的和模值
    int tp=0;
    dp[0][1][tp]=tp;
    dp[1][1][tp]=tp;
    for(int i=1;i<=n;++i){//对分一段的情况初始化
        tp+=a[i]; tp%=p;
        ans[i][1]=tp;
    }
    int now=1;//滚动辅助
    for(int i=1;i<=n;++i){
        for(int j=2;j<=k&&j<=i;++j){//从分两段开始
            for(int l=0;l<p;++l){
                tp=(l-a[i]+p)%p;//tp+a[i]=l 对当前模的结果l而言  其由tp+a[i]得到 即a[i]和前一段模值是DP的前面所有段的和
                dp[now][j][l]=dp[now^1][j][tp]-tp+l;//模数为l
                if(a[i]==l)//单独分组
                    dp[now][j][l]=max(dp[now][j][l],ans[i-1][j-1]+a[i]);
                ans[i][j]=max(ans[i][j],dp[now][j][l]);
            }
        }
        now^=1;//滚动
    }
    cout<<ans[n][k]<<endl;
    return 0;
}

这题还有更优的做法, O(nklogp) O ( n k l o g p ) 即对应套题的下一题

http://codeforces.com/contest/958/problem/C3

暂时还不会

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值