【题解】[SCOI2010]股票交易
\(\text{Solution:}\)
蒟蒻做完这题深知不写清楚\(dp\)转移方程的痛……
首先,显然我们可以设\(dp[i][j]\)表示第\(i\)天,拥有\(j\)股股票的最优解。于是,对于每一天,我们可以:
- 不进行交易
于是直接\(dp[i][j]=\max(dp[i-1][j],dp[i][j])\)即可。
- 只买入股票
直接枚举\(j,dp[i][j]=-ap_i*j\)即可。
- 从之前某一个状态转移过来,并且买股票
首先证明为什么只需要从\(dp[i-w-1][*]\)转移即可。
证明:观察到,每一个状态都记录的是它的最优解。也就说\(dp[i-w-1][*]\)已经把之前的什么\(dp[i-w-...][*]\)的状态转移过了。于是就不必要再去枚举前面那些转移了。
写出转移方程:设我们之前有\(k\)股,买入了\((j-k)\)股。
\[dp[i][j]=\max\limits_{k=0}^{j-c_i} dp[i-w-1][k]-ap_i*(j-k)
\]
显然有我们买的越少花钱越少,单调性显然。单调队列里面存\(k.\)注意边界即可。
- 从之前某一个状态转移过来,并且卖股票
同理,先写出方程:
\[dp[i][j]=\max \limits_{k=j+1}^{j+bs_i} dp[i-w-1][k]+bp*(k-j)
\]
显然卖的越多拿钱越多。单调性显然。
那么对于上面所说两种:买和卖,我们的\(dp\)顺序有区别。
对于买:我们显然由股票少的买到股票多的。于是,我们应该从小到大来\(dp\)股票。
对于卖:我们显然由股票多的卖到股票少的。于是,我们应该从大到小来\(dp\)股票。
所以,上述两种单调队列中所贮存的\(k\)的单调性是不一样的。
最后,弹出队尾的话直接拿\(dp\)方程算结果,取更优者即可。
至于为什么要先加入元素再\(dp:\)
考虑到,在这一刻我们是可以不动自己的股票的。而如果这一刻任何买卖都亏本的话,不如在这里就直接把那些亏本状态舍掉,并把\(j\)加入。当然最后会执行一次转移,但为了保证对头的最优解,我们应当先执行弹出的过程。
#include<bits/stdc++.h>
using namespace std;
int T,w,P,as,bs,ap,bp;
int dp[4000][4000];
int q[4000],head,tail;
int main(){
scanf("%d%d%d",&T,&P,&w);
memset(dp,128,sizeof(dp));
for(int i=1;i<=T;++i){
scanf("%d%d%d%d",&ap,&bp,&as,&bs);
for(int j=0;j<=as;++j)dp[i][j]=-ap*j;
//只买入
for(int j=0;j<=P;++j)dp[i][j]=max(dp[i][j],dp[i-1][j]);
//不做交易
head=1,tail=0;
if(i<=w)continue;//不能交易
for(int j=0;j<=P;++j){
while(head<=tail&&q[head]<j-as)head++;//这里的单调队列递增
while(head<=tail&&dp[i-w-1][q[tail]]-(j-q[tail])*ap<=dp[i-w-1][j])tail--;//如果队尾答案不优秀就一直更新
q[++tail]=j;//元素入队
if(head<=tail)dp[i][j]=max(dp[i][j],dp[i-w-1][q[head]]-(j-q[head])*ap);//若里面还有元素就更新
}
head=1,tail=0;
for(int j=P;j>=0;--j){
while(head<=tail&&q[head]>j+bs)head++;//这里的单调队列递减
while(head<=tail&&dp[i-w-1][q[tail]]+(q[tail]-j)*bp<=dp[i-w-1][j])tail--;
q[++tail]=j;
if(head<=tail)dp[i][j]=max(dp[i][j],dp[i-w-1][q[head]]+(q[head]-j)*bp);
}
}
int ans=-1;
for(int i=0;i<=P;++i)ans=max(ans,dp[T][i]);
printf("%d\n",ans);
return 0;
}