单调队列优化DP

题目传送门

简明题意:你初始时有$∞$ 元钱,并且每天持有的股票不超过 $Maxp$ 。 有$T$ 天,你知道每一天的买入价格 $AP_i$ ,卖出价格 $Bp_i$, 买入数量限制 $AS_i$,卖出数量限制 $BS_i$。 并且两次交易之间必须间隔 $W$天。 现在问你 $T$ 天结束后,最大收益是多少。

我们这么来想,股票买入我们可以看作是负收入

看过数据范围,我们考虑用 $DP$ 来解决这个问题

状态设计:

我们考虑用一个二维数组,显然我们首先需要记录天数,于是第一维就是天数

题目中还有一些限制,就是我们在满足收益最大的前提下,还要满足一些条件:

1,两次交易必须间隔$w$天

2,持有的股票数不得超过给定数

第一个限制条件我们很容易就可以解决,只需要在转移的时候控制一下天数就好了,我们可以直接用$DP$的第一维解决。

第二个限制呢?显然我们需要用一个新的维来记录这个问题,也就是我们需要用第二维来记录手里有多少股票。

于是设 $f[i][j]$ 为$i$天以后拥有$j$张股票所能获得的最大收益。

状态转移:

显然地存在三种大情况:

1,买股票

2,啥也不干

3,卖股票

首先对于买股票,我们又可以有两种,分别是凭空买和本来就有我再买。

啥也不干就显然了,我们就直接把前一天的搞过来就好了

麦股票显然就一种,我只能手里有的时候卖

于是我们有四种情况

case 1: 凭空买

因为直接买,我们就可以把这个初始状态直接赋值,其他的搞成 $-inf$.

$f[i][j]=-ap_i \times j \ ,\ ( 0\le j \le as_i)$

case 2:不买也不卖

直接把上一天的继承过来就可以了

$f[i][j]=max(f[i][j],f[i-1][j]),\ ( 0\le j \le maxp)$

case 3: 在之前的基础买股票

首先关于上面的问题 $1$,我们要满足之间间隔 $w$天,也就是上一次交易一定是在 $i-w-1$天的时候。

为什么我们一定在这一天进行交易?原因是我们在上一个 $case$ 中就解决了这个问题,我们已经把之前某一天以前的最优答案转移到了这一天,所以就不需要考虑以前的天数了,在这一天转移一定是比以前任何一天转移都要优的。

我们已知在第 $i$ 天拥有 $j$ 张股票,假设在第 $i-w-1$ 天有 $k$ 张股票。

$k$ 显然要比 $j$ 小,因为我们要买入,还有一个限制,我们一天最多买 $as_i$ 张,于是我们的 $k$ 的下线就是 $j-as_i$ 。

交易一共买入了 $j-k$ 张股票,要花 $(j-k) \times ap_i $ 元

那么方程就显而易见了:

$f[i][j]=max(f[i-w-1][k]-(j-k) \times ap_i) \ ,\ (j-as_i \le k \le j)$

case 4:卖股票

这个情况和上面那个很类似,但是略有不同。

这次因为要卖股票,所以 $k>j$,并且$k \le j+bs_i$.

本次交易卖出了 $k-j$ 张股票,赚钱 $(k-j) \times bp_i$ 元。

方程:

$f[i][j]=max(f[i-w-1][k]+(k-j) \times bp_i) \ ,\ (j \le k \le j+bs_i)$

优化:

对于 $case\ 3$ 和 $case\ 4$ ,如果我们直接计算,可以发现复杂度大概是 $O(n \times maxp ^2 )$ 的,妥妥的 $T$ 飞。

仔细观察式子:

$case\ 3:$
$f[i][j]=max(f[i-w-1][k]-(j-k) \times ap_i) \ ,\ (j-as_i \le k \le j)$

$case\ 4:$
$f[i][j]=max(f[i-w-1][k]+(k-j) \times bp_i) \ ,\ (j \le k \le j+bs_i)$

先来看$case\ 3$, 我们把括号拆开,得:

$f[i][j]=max(f[i-w-1][k]-j \times ap_i +k \times ap_i) \ ,\ (j-as_i \le k \le j)$

发现其中有一项是不带 $k$ 的,我们把它提出来,得:

$f[i][j]=max(f[i-w-1][k]+k \times ap_i)-j
\times ap_i \ ,\ (j-as_i \le k \le j)$

在给定 $i,j$的时候,只有一个变量 $k$ ,也就是只有$k$ 的取值会影响答案。并且在 $j$ 的循环恰当的时候, $k$ 是具有平移关系的。

或者换句话说:这是符合单调性优化的条件的。

同样的,$case\ 4$ 我们同样可以得出类似的式子:

$f[i][j]=max(f[i-w-1][k]+k \times bp_i)-j \times bp_i \ ,\ (j \le k \le j+bs_i)$

于是我们使用单调队列,每次排除过期元素,然后补上新的选择,最后从对头取出最优解更新即可。

之后就是关于 $j$ 的循环顺序问题,我们要保证上一个状态已经有了。

对于 $case \ 3$ ,我们需要用到 $(j-k)$ 的状态,于是需要正序先算出小的。

对于 $case \ 4$ ,我们需要用到 $(k-j)$ 的状态,于是需要倒叙先算出大的。

于是 $case\ 3$ 正序, $case \ 4$ 倒序。

要说的就这么多了

CODE:

//#define LawrenceSivan

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef unsigned long long ull;
#define INF 0x3f3f3f3f
#define re register
const int maxn=2005;

inline int mymax(int x,int y){
return x>y?x:y;
}

int T,maxp,w,ans;
int ap,bp,as,bs;
int f[maxn][maxn];
int q[maxn],l=1,r=0;

inline int read(){
int x=0, f=1;char ch=getchar();
while (!isdigit(ch)) {if(ch=='-')f=-1;ch=getchar();}
while (isdigit(ch)){x=x*10+(ch^48);ch=getchar();}
return x*f;
}

int main() {
#ifdef LawrenceSivan
freopen("aa.in", "r", stdin);
freopen("aa.out", "w", stdout);
#endif
memset(f,0xcf,sizeof(f));
T=read();maxp=read();w=read();
for(re int i=1;i<=T;i++){
ap=read();bp=read();as=read();bs=read();
for(re int j=0;j<=as;j++){//case 1
f[i][j]=-1*j*ap;
}

for(re int j=0;j<=maxp;j++){// case 2
f[i][j]=mymax(f[i][j],f[i-1][j]);
}

if(i<=w)continue;//如果i<=w,那么i-w-1就是负的了

l=1;r=0;//case 3
for(re int j=0;j<=maxp;j++){//low to high
while(l<=r&&q[l]<j-as)l++;//排除过期元素
while(l<=r&&f[i-w-1][q[r]]+q[r]*ap<=f[i-w-1][j]+j*ap)r--;//更新元素
q[++r]=j;//插入最新元素
if(l<=r)f[i][j]=mymax(f[i][j],f[i-w-1][q[l]]+q[l]*ap-j*ap);// 如果单调队列里有元素,即可转移
}

l=1;r=0;//case 4
for(re int j=maxp;j>=0;j--){//high to low
while(l<=r&&q[l]>j+bs)l++;//排除过期元素
while(l<=r&&f[i-w-1][q[r]]+q[r]*bp<=f[i-w-1][j]+j*bp)r--;//更新元素
q[++r]=j;//插入最新元素
if(l<=r)f[i][j]=mymax(f[i][j],f[i-w-1][q[l]]+q[l]*bp-j*bp);
}
}

for(re int i=0;i<=maxp;i++){//手上可以留下一点
ans=mymax(ans,f[T][i]);
}

printf("%d\n",ans);


return 0;
}

总结:

1,对于决策转移时,有明显单调性的情况,对于状态移动时,转移决策重复度较大。都可用单调队列优化。(滑动窗口)

2,对于单调性要主动发现,主动往那方面想