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\) ;