Hdu3401 Trade(dp 单调队列优化)最详细题解

累加器传送门:

http://blog.csdn.net/NOIAu/article/details/71775000

注:本题代码已经贴在ubuntu pastebin上

代码地址是 http://paste.ubuntu.com/24522455/

题目地址是http://acm.hdu.edu.cn/showproblem.php?pid=3401

Problem Description

Recently, lxhgww is addicted to stock, he finds some regular patterns after a few days’ study.
He forecasts the next T days’ stock market. On the i’th day, you can buy one stock with the price APi or sell one stock to get BPi.
There are some other limits, one can buy at most ASi stocks on the i’th day and at most sell BSi stocks.
Two trading days should have a interval of more than W days. That is to say, suppose you traded (any buy or sell stocks is regarded as a trade)on the i’th day, the next trading day must be on the (i+W+1)th day or later.
What’s more, one can own no more than MaxP stocks at any time.

Before the first day, lxhgww already has infinitely money but no stocks, of course he wants to earn as much money as possible from the stock market. So the question comes, how much at most can he earn?

input

The first line is an integer t, the case number.
The first line of each case are three integers T , MaxP , W .
(0 <= W < T <= 2000, 1 <= MaxP <= 2000) .
The next T lines each has four integers APi,BPi,ASi,BSi( 1<=BPi<=APi<=1000,1<=ASi,BSi<=MaxP), which are mentioned above.

output

The most money lxhgww can earn.

Sample Input

1
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

中文翻译

最近,lxhgww沉迷于股票,经过几天的学习,他发现了一些常规模式。他可以预测未来的T天股市。 在第一天,你可以买价格APi或卖价格BPi的股票。
还有一些其他限制,在一天最多可以买到ASi股票,最多可以卖BSi股票。
两个交易日的间隔应超过W天。 也就是说,假设您在第二天交易(任何买入或卖出股票被视为交易),则下一个交易日必须在第(i + W + 1)天或之后。
而且,任何时候都不能拥有MaxP股票。
在第一天之前,lxhgww已经有无限的钱,但没有股票,当然他想从股票市场赚取尽可能多的钱。 所以问题来了,他能赚多少钱?

大概就是这么道题

这道题首先要构造dp状态转移方程,我们用dp[i][j]来表示第i天,我有j只股票,现在赚了多少钱,为什么这样定义呢,因为如果这样定义,然后转移的话,最后只需要for一遍就知道在最后一天最多盈利了多少

int maxn=-1;
for(int i=1;i<=maxp;i++){
    maxn=max(maxn,dp[T][i]);
}

那么数组定义出来了,怎么进行状态转移呢

我们发现,每次要计算一个dp[i][j]的时候,都要for从开头到i-W-1这么多天,在每一天,手持k张股票(i<=k <=maxp)而k又要分(k>=j)和(k< j)两种情况,因为要讨论是买入还是卖出,还要讨论是不是什么操作都不做,怎么办,好烦啊,代码复杂度也大,时间复杂度也完全不能承受啊啊啊,大概是O(T* T* maxp* maxp)

所以我们来想一下优化方案,因为题目的限制,交易(py)了一次之后,只能在W天之后才能进行交易(交易之后需要休息嘛)所以我们是否可以只讨论W天之前的状态呢,或许可以,但是题目又允许什么都不干啊,我说不定已经W+n天没有交易了(饥渴),那怎么办呢,我们这个时候再新加一个状态转移

dp[i][j]=max(dp[i][j],dp[i-1][j]);

也就是说,我在第i天和第i-1天的时候手持的股票是一样多了,也就是没有买,也没有卖,所以我的i-W-1天是从i-W-2天转移过来的,所以就不用考虑之前的有没有py了嘛,因为已经考虑过啦

那么既然我们只用考虑第i-W-1天的状态了,另外两个(决定第i天是买进还是卖出的)方程也就很好写出来了

dp[i][j]=max(dp[i-W-1][k]-(j-k)*ap[i],dp[i][j]);
//这是买进(1<=k<j)
dp[i][j]=max(dp[i-W-1][k]+(k-j)*bp[i],dp[i][j]);
//这是卖出(j<k<maxp)

我在这里解释一下这两个方程
第i天拥有j只股票赚的钱,等于第(i-W-1)天拥有k只股票,然后在第i天购入(j-k)只股票,当然这里的k需要枚举,假如我i-W-1天有一些钱(可以理解为盈利),然后我在第i天进行了一次买入操作,就花掉了一些钱,与其他情况进行比较(比如我在第i-W-1天买了一些股票(钱先变少了),然后在这一天卖出去),此时需要减去一个花费,也就是我买入股票的价格乘以买的只数,注意这里要满足两个条件,第一个条件是购买的数量不能超过当日最多购买的数量(如果超过了的话,就不能进行这次的更新操作,可以直接continue掉,因为是不可能从dp[i-W-1][k]这个状态转移过来的),另外一个就是不要让持的股超过maxp,也就是j不能超过maxp
满足这两个条件的情况下,进行max操作,卖出是同理的,在第i-W-1天拥有了k只股票,然后卖出k-j只股票,用之前的dp数组的值再加上在第i天卖出k-j只股票盈利的钱,就是这个状态转移过来的值,然后进行更新操作即可

复杂度成功降到少了一个T,很成功,很不错,但是还是要T掉啊,怎么办…w…

这个时候必须再想优化,可是好像已经没有什么可以直接通过更改转移方程的方式优化了,怎么办啊,于是我们想到了用单调队列,用一个单调队列来简化我们要进行maxp次操作的更新,直接找出最大值,然后更新即可
我们再观察一下dp转移方程

dp[i][j]=max(dp[i-W-1][k]-(j-k)*ap[i],dp[i][j]);
//这是买进(1<=k<j)
dp[i][j]=max(dp[i-W-1][k]+(k-j)*bp[i],dp[i][j]);
//这是卖出(j<k<maxp)

如果是买进
dp[i][j]其实是要与(dp[i-w-1][k]-j* ap[i]+k* ap[i])在枚举完所有的k值之后取得的最大值比较最大值进行更新
而dp[i-w-1][k]是一个在k确定的情况下已经确定的值,j*ap[i]也是一个直接确定的值,k*ap[i]是一个在k确定的情况下已经确定的值,其中dp[i-w-1][k]和k*ap[i]和k有关,剩下一个根本就和k无关,这个时候相当于是k在往后推,所以就把dp[i-w-1][k]+k* ap[i]用单调队列来维护就行了

对于每次枚举j的操作,如果是买的话,就建一个单调队列来维护dp[i-W-1][k]+k*ap[i]的最大值,注意,如果是买的话,那么有一个条件,即j减去队首的持有股票,如果大于了每天能买的股票,就把队首指针++;这里就是之前讨论的两个条件之一

对于卖的话,也是维护最大值,只是队首的股票减去我们的j如果大于了第i天最多能卖的股票,就将队首指针++;

无论是买还是卖的队首合法性判断,都用while来写

注意一个问题,如果要满足dp的原理,也就是当前状态是从之前的状态推到之后的状态,我们在买和卖的时候,for的顺序是不一样的,当我们买的时候,需要k小于j的状态都已经计算过了,所以正向for循环,当我们卖的时候需要k大于j的状态已经计算过了,所以反向for循环

最后注意一下初始化问题就好了,因为前W天,我如果买的话,不能进行卖的操作,如果不买的话,我就没有股票,所以不能进行卖的操作,所以这个时候进行初始化,是不能进行卖出操作的

可以用一个结构体来表示这个单调队列,存储值和该状态持有的股票

贴上初始化的代码

scanf("%d%d%d",&T,&MAXP,&W);
        //T表示T天,MAXP表示最多持有多少股票,W表示两次交易之间至少要间隔多少天;
        for(int i=1;i<=T;i++) 
        scanf("%d%d%d%d",&ap[i],&bp[i],&as[i],&bs[i]);
        //ap表示买入价格,bp表示卖出价格,as表示最多买多少,bs表示最多卖多少; 
        for(int i=0;i<=T;i++) dp[i][j]=MINN;
        for(int i=1;i<=T;i++) dp[i][0]=0;//不持股票至少盈利0元(从头到尾不买不卖) 
        for(int i=1;i<=W+1;i++)
            for(int j=1;j<=as[i];j++) 
            dp[i][j]=-j*ap[i];//前W+1天只能买不能卖 
        for(int j=1;j<=MAXP;j++)//也可以不买不卖,这个时候拥有i张股票 
            for(int i=2;i<=W+1;i++) 
            d[i][j]=max(dp[i][j],dp[i-1][j]);
            //从第2天开始枚举,与前一天的状态进行比较
            //这里可能要理解一下,我举个例子,比如我在前一天买了同样数量的股票(因为我们是在比较j相同时的值)
            //而我们只能在预处理的这W天买一次,而先买的可以先卖,所以先买的更便宜的永远是最优的 
        //然后开始进行初始化之后的W+2天到最后一天的dp转移,注意初始化非常重要,到现在初始化就处理完了
        //这个初始化(dp[i][j]=max(dp[i][j],dp[i-1][j]);)也可以放在之后的循环里

然后是初始化之后进行dp转移的代码

for(int i=2;i<=T;i++){
    head=0;tail=0;
    for(int j=0;j<=MAXP;j++) dp[i][j]=Max(dp[i-1][j],dp[i][j]);
    if(i<=W+1) continue;
    for(int j=0;j<=MAXP;j++) modify_buy(i,j); 
    head=0;tail=0;
    for(int j=MAXP;j>=0;j--) modify_sale(i,j);
}

其中modify_buy()函数

while(head<tail&&q[tail-1].value<dp[i-W-1][j]+ap[i]*j) tail--;
//这个时候的k就等于j,因为对于同一个i,j是从0一路枚举过来的 
    q[tail].value=dp[i-W-1][j]+ap[i]*j;
    q[tail].number=j;
    //此时insert了一个k==j时候的状态 
    tail++; 
    //如果我要达到的股票数减去我已经有的股票数大于了我最多能买的股票数
    dp[i][j]=Max(dp[i][j],q[head].value-ap[i]*j );
    //维护了队列之后,队首的就是最大的值 

modify_sale()函数

while(head<tail&&q[tail-1].value<dp[i-W-1][j]+bp[i]*j) tail--;
    q[tail].value=dp[i-W-1][j]+bp[i]*j;
    q[tail].number=j;
    tail++;
    while((head==-1)||(head<tail&&(q[head].number-j)>bs[i])) head++;
    dp[i][j]=Max(dp[i][j],q[head].value-bp[i]*j);

最后进行枚举,计算最大值就可以了

for(int i=0;i<=MAXP;i++)
    maxn=Max(maxn,dp[T][i]);
cout<<maxn<<endl;

贴上完整代码

#include<cstdio>
#include<cstring>
#include<iostream>
#define MINN -1e8 
#define MAXN  12444
#define MAX 2000+10
using namespace std;

int Max(int x,int y){ return x>y?x:y; }
int Min(int x,int y){ return x>y?y:x; }
int head,tail;
int as[MAX],bs[MAX],ap[MAX],bp[MAX];
int dp[MAX][MAX];
int W,t,T,MAXP,d;
struct Queue{
    int value;
    int number;
}q[MAX*2];

void modify_buy(int i,int j){
    while(head<tail&&q[tail-1].value<dp[i-W-1][j]+ap[i]*j) tail--; 
    q[tail].value=dp[i-W-1][j]+ap[i]*j;
    q[tail].number=j;
    tail++; 
    while((head==-1)||(head<tail&&(j-q[head].number)>as[i])) head++;
    dp[i][j]=Max(dp[i][j],q[head].value-ap[i]*j );
}

void modify_sale(int i,int j){
    while(head<tail&&q[tail-1].value<dp[i-W-1][j]+bp[i]*j) tail--;
    q[tail].value=dp[i-W-1][j]+bp[i]*j;
    q[tail].number=j;
    tail++;
    while((head==-1)||(head<tail&&(q[head].number-j)>bs[i])) head++;
    dp[i][j]=Max(dp[i][j],q[head].value-bp[i]*j);
}

int main(){
    scanf("%d",&t);
    while(t--){
        scanf("%d%d%d",&T,&MAXP,&W);
        for(int i=1;i<=T;i++) scanf("%d%d%d%d",&ap[i],&bp[i],&as[i],&bs[i]);
        for(int i=0;i<=T;i++)
            for(int j=0;j<=MAXP;j++) dp[i][j]=MINN;
        for(int i=1;i<=T;i++) dp[i][0]=0; 
        for(int i=1;i<=W+1;i++)
            for(int j=0;j<=as[i];j++) dp[i][j]=-j*ap[i];
        dp[0][0]=0;
        for(int i=2;i<=T;i++){
            head=0;tail=0;
            for(int j=0;j<=MAXP;j++) dp[i][j]=Max(dp[i-1][j],dp[i][j]);
            if(i<=W+1) continue;
            for(int j=0;j<=MAXP;j++) modify_buy(i,j); 
            head=0;tail=0;
            for(int j=MAXP;j>=0;j--) modify_sale(i,j);
        }
        int maxn=MINN;
        for(int i=0;i<=MAXP;i++)
            maxn=Max(maxn,dp[T][i]);
        cout<<maxn<<endl;
    }
    return 0;
}

有一个很关键的地方,我被卡了好久,一定要注意把MINN值开小一些
这里写图片描述

安利一部电影

奇爱博士(1964年)

这是一部黑色幽默,本文章的插图均来自电影截图

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值