[SCOI2010]股票交易(单调队列优化dp)
题目
Description
最近lxhgww又迷上了投资股票,通过一段时间的观察和学习,他总结出了股票行情的一些规律。
通过一段时间的观察,lxhgww预测到了未来T天内某只股票的走势,第i天的股票买入价为每股APi,第i天的股票卖出价为每股BPi(数据保证对于每个i,都有(APi>=BPi),但是每天不能无限制地交易,于是股票交易所规定第i天的一次买入至多只能购买ASi股,一次卖出至多只能卖出BSi股。 另外,股票交易所还制定了两个规定。
为了避免大家疯狂交易,股票交易所规定在两次交易(某一天的买入或者卖出均算是一次交易)之间,至少要间隔W天,也就是说如果在第i天发生了交易,那么从第i+1天到第i+W天,均不能发生交易。同时,为了避免垄断,股票交易所还规定在任何时间,一个人的手里的股票数不能超过MaxP。
在第1天之前,lxhgww手里有一大笔钱(可以认为钱的数目无限),但是没有任何股票,当然,T天以后,lxhgww想要赚到最多的钱,聪明的程序员们,你们能帮助他吗?
对于
30% 的数据,0≤W<T≤50,1≤MaxP≤50
对于 50% 的数据,0≤W<T≤2000,1≤MaxP≤50
对于 100% 的数据,0≤W<T≤2000,1≤MaxP≤2000
对于所有的数据,1≤BPi≤APi≤1000,1≤ASi,BSi≤MaxP
Input
输入数据第一行包括3个整数,分别是T,MaxP,W。 接下来T行,第i行代表第i-1天的股票走势,每行4个整数,分别表示APi,BPi,ASi,BSi。
Output
输出数据为一行,包括1个数字,表示lxhgww能赚到的最多的钱数。
Sample Input
5 2 0 2 1 1 1 2 1 1 1 3 2 1 1 4 3 1 1 5 4 1 1
Sample Output
3
思路
这是一道dp题;
我们设dp[i][j] 表示第 i 天 lxhgww手中拥有 j 个股票,能产生的最大money;
那么
dp初始化是
memset(dp,-127/3,sizeof(dp)); for(re ll j=0;j<=As[i];j++) dp[i][j]=-1*j*Ap[i];//第 i 天把 j 个股票全买光
转移方程是
$dp[i][j]=dp[i-1][j];$
第 i 天啥也没做,money还是那么多;
$dp[i][j]=max(dp[i-w-1][k]-(j-k)*Ap[i]);$
第 i 天买了些股票,money变少了 =_=
$dp[i][j]=max(dp[i-w-1][k]+(k-j)*Bp[i]);$
第 i 天卖了些股票,money回来了¥_¥!!!
很显然在第二个转移方程里$ j-k 必须 < As[i]$ 当天买的股票数不能超过题目中的限制;
很显然在第三个转移方程里$ k-j 必须 < Bs[i]$ 当天卖的股票数不能超过题目中的限制;
但是我们发现,这一题如果直接dp写的话,会超时!!
那么就需要单调队列来优化;
观察两个dp 方程:
dp[i][j]=max(dp[i-w-1][k]-(j-k)*Ap[i]);
dp[i][j]=max(dp[i-w-1][k]+(k-j)*Bp[i]);
我们可以转化一下;
dp[i][j]=max(dp[i-w-1][k]+k*Ap[i] -j*Ap[i]);
dp[i][j]=max(dp[i-w-1][k]+k*Bp[i] -j*Bp[i]);
这样就可以把方程分成两部分;
我们发现划横线的部分只与 k 有关;
这样我们希望money最大,那么画线的部分就要最大;
这样我们就可以把 k 存到一个单调队列里;
找出最大的 $dp[i-w-1][k]+k*Ap[i];$ 就可以了;
然后什么时候踢队头呢;
在前面列出的dp方程中,我们标记了范围;
第二个转移方程里 $j-k 必须 < As[i]$
第三个转移方程里 $k-j 必须 < Bs[i] $
所以
1 while(head<=tail&&j-q[head]>As[i]) head++; 2 while(head<=tail&&q[head]-j>Bs[i]) head++;
这样就ok了
渣渣代码
#include<bits/stdc++.h> #define re register typedef long long ll; using namespace std; inline ll read() { ll a=0,f=1; char c=getchar(); while (c<'0'||c>'9') {if (c=='-') f=-1; c=getchar();} while (c>='0'&&c<='9') {a=a*10+c-'0'; c=getchar();} return a*f; }//无处不在的快读 ll n,m,w; ll q[10010],dp[2010][2010]; ll Ap[2010],Bp[2010],As[2010],Bs[2010]; int main() { memset(dp,-127/3,sizeof(dp)); n=read(); m=read(); w=read(); for(re ll i=1;i<=n;i++) { Ap[i]=read(); Bp[i]=read(); As[i]=read(); Bs[i]=read();//读入 } for(re ll i=1;i<=n;i++) { for(re ll j=0;j<=As[i];j++)//注意j <=As[i]; dp[i][j]=-1*j*Ap[i];//初始化 第 i 天把 j 个股票全买光 for(re ll j=0;j<=m;j++) dp[i][j]=max(dp[i][j],dp[i-1][j]);//第 i 天啥也没做,money还是那么多; if(i-w-1<=0)//注意一定要加,判下标负数 continue; ll head=1,tail=0; for(re ll j=0;j<=m;j++) { while(head<=tail&&j-q[head]>As[i])// j-k 必须 < As[i] head++;//踢队头 if(head<=tail) { ll k=q[head]; dp[i][j]=max(dp[i][j],dp[i-w-1][k]-(j-k)*Ap[i]);//dp 转移方程 } while(head<=tail&&dp[i-w-1][q[tail]]+q[tail]*Ap[i]<dp[i-w-1][j]+j*Ap[i])//找一个最大值 tail--; q[++tail]=j;//入队 } head=1,tail=0; for(re ll j=m;j>=0;j--)//我们需要 q[head](k) > j 所以先入队 大的j ,这样就可以满足q[head](k) > j { while(head<=tail&&q[head]-j>Bs[i])// k-j 必须 < Bs[i] head++;//踢队头 if(head<=tail) { ll k=q[head]; dp[i][j]=max(dp[i][j],dp[i-w-1][k]+(k-j)*Bp[i]);//dp 转移方程 } while(head<=tail&&dp[i-w-1][q[tail]]+q[tail]*Bp[i]<dp[i-w-1][j]+j*Bp[i])//找一个最大值 tail--; q[++tail]=j;//入队 } } ll ans=1>>30;//方便的右移符号 for(re ll i=0;i<=m;i++) ans=max(ans,dp[n][i]);//统计ans printf("%lld\n",ans);//输出 return 0;//不要忘了return 0;^_^ } //然后AC