P5662 纪念品 题解

P5662 纪念品 题解

原文链接:http://suo.im/5PMmIG

看到题目就感觉到一定是动态规划了,但是怎么定义状态呢?到每一天的时候,手里不同纪念品的数量有很多,要是每种情况都存下来,状态爆炸多,必死无疑啊。

这题感觉跟买卖股票很像啊,回忆一下我平时怎么炒(pei)股(qian)的呢?就是频繁交易嘛,今天买了,明天看到涨就卖,看到跌了也忍不住要卖。可惜我没有小伟的超能力啊,这里实名羡慕。

这题题面有一句关键的话,“当日购买的纪念品也可以当日卖出换回金币”!这句话可以帮我们简化状态,因为如果一个纪念品,你想连续持有若干天,可以看做第一天买,第二天早上立刻卖掉,然后第二天买回来,第三天早上立刻卖掉,然后第三天买回来……所以我们就不需要记录每天手里持有多少纪念品了,统一认为我们今天买的纪念品,明天早上就立刻卖掉。明天又是新的一天,用所有的现金,进行新的决策就好了。

我们定义一个三维的数组, \(dp[i][j][k]\)表示第i天,我们考虑到第j个物品的时候,手里现金还有k元的时候,明天早上全卖掉能拿到的金币数。类似完全背包的思路,就可以写递推了。我们用 \(price[i][j]\)表示第i天第j个物品的价格,外层循环i,里层循环每个物品j,手里留k元现金,则 $$dp[i][j][k]=max(dp[i][j][k],dp[i][j-1][k+price[i][j]]+price[i+1][j]-price[i][j])$$ 表示第j个物品如果要了,手里现金少了 \(price[i][j]\),但是期望明天早上的收益多了 \(price[i+1][j]-price[i][j])\)

j循环完一遍以后,在收益里面取最大值,变成下一天的开始金币数。

但是这样开三维数组会炸空间,没关系,见过世面的我根本不慌。因为从第i天传递到第i+1天,只需要传递一个数字,即最大收益。如果第二题早上都卖掉有多重选择,为啥不选最赚钱的呢,是吧?所以第一个维度可以压掉。第二个维度,多重背包可以循环的时候控制循环方向压一维,相信学过完全背包的同学都会。所以其实数组只有一维就够了,表示手里现金数,按照题目说明,不会超过10000

其余细节见代码注释:

#include <iostream>
#include <cstring>
#include <cstdio>

using namespace std;
const int MAXN = 105;

//dp[k]表示手里剩k元现金的时候,明天早上都卖了以后的钱数
//price[i][j]表示第i天第j件物品的价格
int dp[10005], price[MAXN][MAXN];

int main() {
    int t, n, m, ans;
    scanf("%d%d%d", &t, &n, &m);
    //先输入
    for (int i = 1; i <= t; ++i) {
        for (int j = 1; j <= n; ++j) {
            scanf("%d", &price[i][j]);
        }
    }
    //第一天早上手里有m元
    ans = m;
    for (int i = 1; i < t; ++i) {
        //先把数组赋值为负无穷
        memset(dp, ~0x3f, sizeof(dp));
        //什么都不买,今天早上有ans元,明天早上也是ans元
        dp[ans] = ans;
        //枚举第j个物品
        for (int j = 1; j <= n; ++j) {
            //手里有k元的时候,去推明天早上的钱
            for (int k = ans; k >= price[i][j]; --k) {
                //买一件物品,现金减少,赚一份差价,完全背包倒着循环
                dp[k - price[i][j]] = max(dp[k - price[i][j]], dp[k] + price[i + 1][j] - price[i][j]);
            }
        }
        //找一下明天早上收益最大
        int ma = 0;
        for (int j = 0; j <= ans; ++j) {
            ma = max(ma, dp[j]);
        }
        //明天早上就有这么多钱了,继续赚钱
        ans = ma;
    }
    cout << ans << endl;
    return 0;
}

CSP2019-J T3 纪念品(souvenir)题解

原文链接:http://suo.im/5PMnvy

温馨提示:请在仔细阅读题目后食用

前言

看到题目时,直觉告诉我这一定是 DP

--> 自闭 --> 看 T4 --> 再次自闭 --> 回来骗分

然后在做 \(T=2\) 的部分分时顿悟……

没有思路时,为什么不试试上厕所和拿部分分呢?

又:结合 P2938 食用体验更佳

正文

I) \(T=1(10\%)\)

一点想法也没有……就骗骗分吧。

此时,小伟购买纪念品后须在当天卖出,总金币数不变。直接输出 \(M\)

参考代码略。


II) \(T≤100,N=1(15\%)\)

更贪心地骗分。

只有一件纪念品时,容易想到贪心。

\(P_i\) 表示第 \(i\) 天该纪念品的价值。

\(P_i<P_{i+1}\) 时,选择在第 \(i\) 天尽可能地购入纪念品并在第 \(i+1\) 天全部售出。反之,选择不购入。

\(P_i<P_{i+1}<...<P_{i+k}\) 时,发现第 \(i+1\) 天至第 \(i+k-1\) 天的“售出-重新购入”的过程是多余的。可以将整个过程合并为在第 \(i\) 天购入并在 \(i+k\) 天售出。

那么,我们可将 \(P_i(1\le i\le T)\) 划分为若干个连续上升子序列,在每一个子序列的首端尽可能地购入纪念品并在其末端全部售出。

参考代码:

#include<stdio.h>
const int MAXN=105;
int p[MAXN];
int main(){
    int T,N,M;
    scanf("%d%d%d",&T,&N,&M);
    for(int i=1;i<=T;++i){
        scanf("%d",p+i);
    }
    for(int i=1;i<T;++i){
        if(p[i]<p[i+1]){
            int tot=M/p[i];
            //储存购入的纪念品数
            M%=p[i];
            while(i<T&&p[i]<p[i+1]){
                ++i;
            }
            M+=tot*p[i];
            //售出纪念品
        }
    }
    printf("%d\n",M);
    return 0;
}

时间复杂度为 \(O(T)\)


III) \(T=2,N≤100(15\%)\)

简单的 15 分,启发了我们。

看到 \(T=2\) ,容易想到将每件纪念品的价格增量求出。

但是,每件纪念品的购入价格怎么处理?

——欸,这不是背包问题吗?

每件纪念品的价格增量 作为其价值,每件纪念品在第一天购入的价格 作为其质量, \(M\) 作为背包容量,这个问题就转化为了一个完全背包问题。

参考代码:

#include<stdio.h>
#include<string.h>
const int MAXN=105;
const int MAXM=10005;
//注意范围
int v[MAXN],w[MAXN],f[MAXM];
int main(){
    memset(f,0,sizeof(f));
    int T,N,M,tmp;
    scanf("%d%d%d",&T,&N,&M);
    for(int i=1;i<=N;++i){
        scanf("%d",w+i);
    }
    for(int i=1;i<=N;++i){
        scanf("%d",&tmp);
        v[i]=tmp-w[MAXN];
    }
    //计算价格增量
    for(int i=1;i<=N;++i){
        if(v[j]<0){     
            continue;
        }
        //略去价格增量为负的纪念品
        for(int j=w[i];j<=M;++j){
            if(f[j]<f[j-w[i]]+v[i]){
                f[j]=f[j-w[i]]+v[i];
            }
        }
    }
    //完全背包
    printf("%d\n",f[M]);
    return 0;
}

时间复杂度为 \(O(N\times M)\)


IV) \(T≤100,N≤100,M≤10^3(100\%)\)

III) 之后,就容易想到进行 \(T-1\) 次完全背包来求出答案了。

在敲正解前,请看下面这组数据:

3 3 80
12 20 15
10 17 13
15 25 16

“第二天的收益值应取负数还是取0?如果取0,跳过‘第二天’,第三天又如何计算?”

事实上,“跳过第二天”的说法并不准确。

除第一天只有购入过程、最后一天只有售出过程外,每天都有售出与购入两个过程。两个过程互不干扰。

为获得更多的“资金”,不妨令每日的售出过程先于购入过程。

每天的购入过程与次日的售出过程(差价)构成一次完全背包。或者说,完全背包是在“第 *X.5* 天”进行的。

也就是:

Day(1).购入
Day(2).售出
//完全背包
Day(2).购入
Day(3).售出
//完全背包
......
Day(T-1).购入
Day(T).售出
//完全背包

而我们跳过的是第一天的购入过程与第二天的售出过程,第二天的购入过程与第三天的售出过程并不受影响。

以此来建立完全背包模型,是最关键、最难理解的地方。

参考代码:

#include<stdio.h>
#include<string.h>
const int MAXN=105;
const int MAXM=10005;   
//背包的最大容量应为10^4,不同于M的10^3
inline int in(){
    char ch=getchar();
    int s=0;
    while(ch<'0'||ch>'9'){
        ch=getchar();
    }
    while(ch>='0'&&ch<='9'){
        s=(s<<3)+(s<<1)+(ch^48);
        ch=getchar();
    }
    return s;
}
//快读
int w[MAXN],t[MAXN],v[MAXN],f[MAXM];
//t临时储存次日的购入价格
int main(){
    int T=in(),N=in(),M=in();
    if(T==1){
        printf("%d\n",M);
        return 0;
    }
    //特判T=1的情况(可略去)
    for(int i=0;i<N;++i){       
        w[i]=in();
    }
    //第一天无售出过程,直接储存其购入价格
    for(int i=1;i<T;++i){
        for(int j=0;j<N;++j){       
            t[j]=in();
            v[j]=t[j]-w[j];
        }
        //输入次日纪念品的价格,计算当日购入该纪念品的净收益
        memset(f,0,sizeof(f));
        //清零
        for(int j=0;j<N;++j){
            if(v[j]<0){     
                continue;
            }
            //略去当日净收益为负的纪念品
            for(int k=w[j];k<=M;++k){
                if(f[k]<f[k-w[j]]+v[j]){
                    f[k]=f[k-w[j]]+v[j];
                }
            }
        }
        //完全背包
        M+=f[M];    
        //金币数增加,同时背包容量(即次日可使用的金币数)增加
        for(int j=0;j<N;++j){
            w[j]=t[j];
        }
        //“购入-售出”过程结束后将当前的“售出”价格转为次日的“购入”价格
    }
    printf("%d\n",M);
}

时间复杂度为 \(O(T\times N\times MAXM)\)

后记

第一篇题解,如有不足请指出。

考试时误认为 \(MAXM\) 的最大值等于 \(M\) 的最大值……

Failed to AK.

最后,迟到的……

CSP2019 RP++


更新日志

2019.11.17 初稿;

2019.11.18 修订部分错误,完善并调整部分描述,完善排版,补充时间复杂度;

2019.11.24 修订部分错误,简化并调整部分描述,完善排版;

2020.1.2 调整部分描述,调整排版,补充 \(\mathrm L_AT^EX\)

posted @ 2020-10-15 20:25  SweepyZhou  阅读(330)  评论(0)    收藏  举报