题目传送门https://www.luogu.org/problemnew/show/P2569
第一眼看题就觉得是个dp ,然后看到2000的范围,hmm大概是个n^2的2维dp
开始设状态,第一维肯定是天数的枚举,第二维...看着MaxP的范围,觉得应该是持有股票数量的枚举
那么dp[i][j]就是对于1~i天,第i天持有j张股票的情况下最多能赚的钱
然后想转移方程,分成几种情况
case 1:前面都不买,从第i天开始买
也就是从一无所有的情况下买j张股票dp[i][j]=-APi*j
case 2:不买不卖
这种情况下不会就不会受到W的限制,直接dp[i][j]=dp[i-1][j]
case 3:发生买卖
首先要考虑应该从前面哪一天的状态转移过来,也就是上一次买卖在哪一天发生。
乍一看好像没办法知道应该从哪一天转移,莫非又多一层1~i-W-1的循环?
其实不用。上一次买卖所获得的最大值可以根据case 2的dp方程向后转移
所以直接从i-W-1天转移就行
case 3.1:又卖又买
仔细想想,这种情况不可能成为最大值。因为有APi>=BPi,卖了又买不如买少点,多折腾是不行的
case 3.2:只买不卖
既然是买,那么之前持有的股票必然比j少,枚举之前持有的股票k,花钱(j-k)*APi
dp[i][j]=dp[i-W-1][k]-(j-k)*APi;
case3.3:只卖不买
与case 3.2类似
dp[i][j]=max dp[i-W-1][k]+(k-j)*BPi
大概就是这样。dp[T][0]就是最终答案(保险起见我写的是max dp[T][0~MaxP])
时间大概是O(n*MaxP^2)
预期50pt做法(实际70pt)
#include<iostream> #include<cstdio> #include<cmath> #include<cstring> #include<algorithm> using namespace std; int T,MaxP,W; struct pig { int APi,BPi,ASi,BSi; } day[2005]; int dp[2005][2005]; int main() { cin>>T>>MaxP>>W; for(int i=1; i<=T; i++) { scanf("%d%d%d%d",&day[i].APi,&day[i].BPi,&day[i].ASi,&day[i].BSi); } for(int i=0; i<=T; i++) { for(int j=0; j<=MaxP; j++) { dp[i][j]=-999999999; } } dp[0][0]=0; for(int i=1;i<=T;i++){ for(int j=0;j<=MaxP;j++){ dp[i][j]=dp[i-1][j]; if(j<=day[i].ASi){ dp[i][j]=max(dp[i][j],-j*day[i].APi); } if(i-W-1<0){ continue; } for(int k=j-day[i].ASi;k<j;k++){ if(k<0){ continue; } dp[i][j]=max(dp[i][j],dp[i-W-1][k]-(j-k)*day[i].APi); } for(int k=j+1;k<=j+day[i].BSi;k++){ if(k>MaxP){ break; } dp[i][j]=max(dp[i][j],dp[i-W-1][k]+(k-j)*day[i].BPi); } } } int ans=0; for(int j=0; j<=MaxP; j++) { ans=max(ans,dp[T][j]); } cout<<ans; return 0; } /* 输入 初始化 赋-inf,dp[0][0]=0 dp循环 i 天数1-T j 当前持股数0-Maxp 从当前天开买 j<=Asi dp[i][j]=-j*APi; 不买不卖 dp[i][j]=dp[i-1][j] 如果i-W-1>=0 k 前次购买时持股数 买股 (j-Asi)~j&&>=0 dp[i][j]=max dp[i-W-1][k]-(j-k)*APi; k 前次购买时持股数 卖股 j~(j+Bsi)&&<=MaxP dp[i][j]=max dp[i-W-1][k]+(k-j)*BPi 输出,max(dp[0][0~MaxP]) 自测数据 2 1 1 1 1 1 1 1 2 1 1 3 1 1 4 1 1 2 0 1 1 1 1 1 1 2 1 1 3 1 1 4 1 1 */
怎么继续优化呢?
如果仍然保持dp[i][j]的形式,i和j的循环肯定是不能去掉的,所以考虑优化k
用case 3.2:只买不卖 举例
它的dp方程是dp[i][j]=max dp[i-W-1][k]-(j-k)*APi;
乘法分配律得dp[i][j]=max dp[i-W-1][k]+k*APi-j*APi;
j是枚举出来的,所以可以拆分一下max,得dp[i][j]=max(dp[i-W-1][k]+k*APi)-j*APi;
因为对于每个确定的i和j,k属于区间 [j - ASi ,j-1] ,假设i是定值,ASi就是定值。那么对于所有j,k的区间大小是一样的
一个长度固定的区间一格一格的移动...这不就是滑动窗口吗
单调队列优化,O(n*MaxP),100pt到手
#include<iostream> #include<cstdio> #include<cmath> #include<cstring> #include<algorithm> using namespace std; int T,MaxP,W; struct MonoQue { int lim;//维持的长度 int size;//最大存储量 int que[2005];//单调队列 int data[2005];//维护的数据 int head,tail; void init(int mys,int mylim) { size=mys; lim=mylim; head=0; tail=0; } void push(int id) {//向单调队列中加入并维护,id可以在特殊情况下跳跃前进 if(id<=size) {//防越界处理 for(; head<tail;) { if(data[que[tail-1]]>data[id]) { break; } tail--; } que[tail]=id; tail++; } if(que[head]+lim<=id) { pop(); } } void pop() { head++; } int quetop() { return que[head]; } int top() { return data[quetop()]; } }; struct pig { int APi,BPi,ASi,BSi; } day[2005]; int dp[2005][2005]; int main() { cin>>T>>MaxP>>W; for(int i=1; i<=T; i++) { scanf("%d%d%d%d",&day[i].APi,&day[i].BPi,&day[i].ASi,&day[i].BSi); } for(int i=0; i<=T; i++) { for(int j=0; j<=MaxP; j++) { dp[i][j]=-999999999; } } dp[0][0]=0; MonoQue buy,sell; for(int i=1; i<=T; i++) { buy.init(MaxP,day[i].ASi+1); sell.init(MaxP,day[i].BSi+1); if(i-W-1>=0) {//防数组超下界 for(int j=0; j<=MaxP; j++) { buy.data[j]=dp[i-W-1][j]+j*day[i].APi; sell.data[j]=dp[i-W-1][j]+j*day[i].BPi; } } for(int j=0; j<day[i].BSi; j++) { sell.push(j);//sell需要预先加入 } for(int j=0; j<=MaxP; j++) { dp[i][j]=dp[i-1][j]; if(j<=day[i].ASi) { dp[i][j]=max(dp[i][j],-j*day[i].APi); } if(i-W-1<0) { continue; } buy.push(j); dp[i][j]=max(dp[i][j],buy.top()-j*day[i].APi); sell.push(j+day[i].BSi);//单调队列里会处理超了MaxP的情况 dp[i][j]=max(dp[i][j],sell.top()-j*day[i].BPi); } } int ans=0; for(int j=0; j<=MaxP; j++) { ans=max(ans,dp[T][j]); } cout<<ans; return 0; } /* 自测数据 3 2 0 2 1 1 1 3 2 1 1 4 3 1 1 */
参考: