单调队列优化dp详解

要想了解单调队列,我们得要先看一道题来明白.我们可以先看看这道叫做“我要长高”的题目.

Description
韩父有N个儿子,分别是韩一,韩二…韩N。由于韩家演技功底深厚,加上他们间的密切配合,演出获得了巨大成功,票房甚至高达2000万。舟子是名很有威望的公知,可是他表面上两袖清风实则内心阴暗,看到韩家红红火火,嫉妒心遂起,便发微薄调侃韩二们站成一列时身高参差不齐。由于舟子的影响力,随口一句便会造成韩家的巨大损失,具体亏损是这样计算的,韩一,韩二…韩N站成一排,损失即为C*(韩i与韩i+1的高度差(1<=i< N))之和,搞不好连女儿都赔了.韩父苦苦思索,决定给韩子们内增高(注意韩子们变矮是不科学的只能增高或什么也不做),增高1cm是很容易的,可是增高10cm花费就很大了,对任意韩i,增高Hcm的花费是H^2.请你帮助韩父让韩家损失最小。

Input
有若干组数据,一直处理到文件结束。 每组数据第一行为两个整数:韩子数量N(1<=N<=50000)和舟子系数C(1<=C<=100) 接下来N行分别是韩i的高度(1<=hi<=100)。

我们可以看到这道题:dp!怎么d?想怎么d怎么d。。。好吧其实我们可以分析一下————很容易想到在枚举每个儿子的时候,当前儿子的花费都会受到且只受到前一个儿子的影响,可以建一个dp[i][j]表示当前第i个孩子身高为j的情况。状态转移方程为dp[i][j]=min(dp[i-1][k] + abs(j-k)C + (a[i]-j)(a[i]-j)) a[i]是当前枚举人本身的身高,我们知道他们都可以增高,所以你在状态转移的时候,需要枚举前一个人的身高和这个人的身高k和j,a[k]<=k<=100,a[j]<=j<=100(注意只能增高——可能穿了什么恨天高牌增高鞋),这样dp确实可以d,但是不要高兴的太早了——我们来分析一下时间复杂度——我们要枚举每个人,枚举到每个人的时候还要枚举当前人的身高,还有之前人的身高,从给出的Input数据来看,在两秒的时间限制内————超时!怎么办,玫瑰色的人生还没有开始就结束了,我能怎么办,我也很绝望…

不过不用着急,我们可以用单调队列来帮你解决.分析一下这个方程,我们可以看到有一个abs的东西,即是绝对值,所以我们来分析两种情况,一种是前面那个人比当前枚举的人高,一种前面那个人比当前枚举的人矮,这样我们就可以吧绝对值的帽子去掉,建设新农村…好吧优化dp的新办法。我们看一下去掉绝对值的方程.
当第 i 个儿子的身高比第 i-1 个儿子的身高要高时,
dp[i][j]=min(dp[i-1][k] + j*C-k*C + X); ( k<=j ) 其中 X=(x[i]-j)*(x[i]-j)。
当第 i 个儿子的身高比第 i-1 个儿子的身高要矮时,
dp[i][j]=min(dp[i-1][k] - j*C+k*C + X); ( k>=j ).

我们便可以在去掉绝对值的情况下进行分类讨论.首先,我们先令一个二维数组f,f[i-1][k]=dp[i-1][k]-k*C, g[i][j]=j*C+X; 于是dp[i][j] =min (f[i-1][k])+g[i][j]。

在两个取绝对值的式子里面,我们可以看到都需要前一个人的花费求到一个最小值来更新当前状态,那我们可以建造一个单调递增的队列,在枚举第i个人的时候,我们把前一个人(i-1)在不同身高的状态所用的花费做成一个这样的单调队列,这样就不用枚举前一个人的身高(因为我们想要的最小值已经求出来了),所以就大大减少了时间复杂度。代码如下.

#include
#include
using namespace std;
const int inf=0x3f3f3f3f;
int dp[2][101],q[101],h,t,cur,x,n,c,ans,k;
inline const int read()
{
  register int x=0,f=1;
  register char ch=getchar();
  while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
  while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
  return x*f;
}//读入优化
int main()
{ 
  while(scanf("%d%d",&n,&c)!=EOF)
  { ans=inf;
    x=read();
    cur=0;//cur 是为了滚动数组的 
    for(int i=0;i<x;i++) dp[cur][i]=inf;//不能变矮,赋为超大值
    for(int i=x;i<=100;i++) dp[cur][i]=(i-x)*(i-x);//增高的花费
    for(register int i=1;ix=read(),cur=cur^1,h=1,t=0;//单调队列初始化,cur有涉及到位运算,cur^1可以看做1-cur
      for(int j=0;j<=100;j++)//前一个比当前高 
      {
        k=dp[cur^1][j]-j*c;
        while(h<=t&&q[t]>k) t--;//如果队尾比当前大就弹出队列,为什么?我们维护的是一个单调递增的序列,队首保存最优,为了满足这个性质,不优的全部踢掉
        q[++t]=k;//保存当前元素
        if(j<x) dp[cur][j]=inf;
        else dp[cur][j]=q[h]+j*c+(x-j)*(x-j);
        //可以代入上面的方程看看,好理解一点
      }  
      h=1,t=0;
      for(int j=100;j>=0;j--)//前一个比当前矮 
      {
        k=dp[cur^1][j]+j*c;
        while(h<=t&&q[t]>k) t--;
        q[++t]=k;
        if(j>=x) dp[cur][j]=min(dp[cur][j],q[h]-j*c+(x-j)*(x-j));//直接比较求最优了
        //这就是单调队列的好处,我们可以直接调用q[h],因为我们维护的是一个单调递增的序列,队首的自然是最小的,即min值,不需再枚举前一个人的身高,直接调用!
      }  
    } 
    for(int i=0;i<=100;i++) ans=min(ans,dp[cur][i]);
    printf("%d\n",ans);
  }
   return 0;
}

不过这道题其实是可以不用单调队列的,因为我们只需要保存最小值就可以了…用一个Min不断比较保存前一个人的最小花费即可,代码如下(不过单调队列的思想很重要!)

#include 
#include 
#include 
#include 
#define inf 0x3fffffff
using namespace std;
int n,c,h[50005],dp[2][105],cur,limit,Min;
inline const int read()
{
  register int x=0,f=1;
  register char ch=getchar();
  while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
  while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
  return x*f;
}
int main()
{
    while (scanf("%d%d",&n,&c)==2) {
        cur=1;
        limit=-inf;
        Min=inf;
        for(register int i=1;i<=n;++i){h[i]=read();limit=max(limit,h[i]);} 
        for(register int i=0;i<=1;++i) memset(dp[i],0x3f,sizeof(dp[i]));     
        for(int i=h[1];i<=limit;++i) dp[cur][i]=(i-h[1])*(i-h[1]); 
        for(register int i=2;i<=n;++i) { 
            cur=cur^1;
            Min=inf;
            for(int j=h[i-1];j<=limit;++j) {
                Min=min(Min,dp[cur^1][j]-c*j); 
                if (j>=h[i]) dp[cur][j]=Min+c*j+(j-h[i])*(j-h[i]);
            }
            Min=inf;
            for(int j=limit;j>=h[i];--j) {
                Min=min(Min,dp[cur^1][j]+c*j);
                if(j>=h[i]) dp[cur][j]=min(dp[cur][j],Min-c*j+(j-h[i])*(j-h[i]));
            }
            memset(dp[cur^1],0x3f,sizeof(dp[cur^1]));
        }
        for (int i=h[n];i<=limit;++i) {
            Min=min(Min,dp[cur][i]);
        }
        printf("%d\n",Min);
    }
    return 0;    
}

相信大家对单调队列有了一个初步的了解,说白了,就是少一种枚举,减去一维,直接可以获取单调队列里保存的最优解,但注意,并非所有的dp都可以用单调队列来优化,只有状态转移方程中出现了求min或max的时候才可能可以用,因为这样才符合单调队列里的性质。

现在我们进行深入,做一道很经典的题,这道题描述的是一个股神,可以预测股市行情,但是这个天才都这么牛了还需要我们帮他计算最大利润…这道题是hdu3401,可以自己看看题目大意(不过是英文的哦).

分析一下这道题,很容易知道在第i天做当前决策的时候,我们有三个选择,一是从前一天不买也不卖,二是从前i-W-1天买进一些股(w是冷冻时间,你进行一次股市操作就要至少w天后才能再次进行一次操作——注意,每次操作只能是买或者是卖,当然,你也可以不买不卖.),三是从i-W-1天卖掉一些股.问什么一定是i-w-1天呢?因为有第一种情况我们知道,在i-w-2天时我们可以通过不买也不卖来讲最优状态转移到i-w-1天,所以没必要考虑i-w-1天之前的了.
分析完情况,我们便可以想到方程。这道题的买和卖可以分成两个方程,跟上面那道我要长高分成前一个人比我高或者比我矮一样的道理.我们来看一看买的状态转移方程.

dp[i][j]=max(dp[i-W-1][k]+k*AP[i])-j*AP[i]

我们发现了max符号——求最大值?是不是意味着我们只要提前用单调队列求出最大值即可?所以我们可以令f[i-W-1][k]=dp[i-W-1][k]+k*AP[i],则dp[i][j]=max(f[i-W-1][k]) - j*AP[i]。把f数组用一个单调递减的序列维护(队首保留最大值)即可,代码如下.

#include
#include
using namespace std;
int t,ap,bp,as,bs,dp[2004][2004],q[2004],v[2004],l,r;
int n,maxp,w;
inline const int read()
{
   register int x=0,f=1;
   register char ch=getchar();
   while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
   while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
   return f*x;
}
int main()
{
   t=read();
   while(t--)
   {
    n=read(),maxp=read(),w=read();   
    register int i,j;
    for(i=1;i<=w+1;i++)
    {
     ap=read(),bp=read(),as=read(),bs=read();
     for(j=0;j<=maxp;j++)
     {
      if(j<=as) dp[i][j]=-j*ap;
      else dp[i][j]=-210000000;
      if(i>1) dp[i][j]=max(dp[i][j],dp[i-1][j]);
     }
    }
    for(register int i=w+2;i<=n;i++)
    {
     ap=read(),bp=read(),as=read(),bs=read();
     int k=i-w-1;
     l=0,r=-1;
     for(j=0;j<=maxp;j++)
     { //int tmp=dp[k][j]-ap*(maxp-j);
       while(l<=r&&tmp>v[r]) r--;
       q[++r]=j;
       v[r]=tmp; //v数组保存的是值
       while(j-q[l]>as) l++;
       dp[i][j]=max(dp[i-1][j],v[l]+ap*(maxp-j));
     }
     l=0,r=-1;
     for(j=maxp;j>=0;j--)
     { //int tmp=dp[k][j]+bp*j;
       while(l<=r&&tmp>v[r]) r--;
       q[++r]=j;
       v[r]=tmp;
       while(q[l]-j>bs) l++;
       dp[i][j]=max(dp[i][j],v[l]-bp*j);
     }
    }
    printf("%d\n",dp[n][0]);
   }
}

最后一道,NOI2009的经典题,瑰丽华尔兹.这道题的描述有点长,可以直接看主干部分.
【任务描述】

你跳过华尔兹吗?当音乐响起,当你随着旋律滑动舞步,是不是有一种漫步仙境的惬意?众所周知,跳华尔兹时,最重要的是有好的音乐。但是很少有几个人知道,世界上最伟大的钢琴家一生都漂泊在大海上,他的名字叫丹尼•布德曼•T.D.•柠檬•1900,朋友们都叫他1900。
1900出生于20世纪的第一年出生在往返于欧美的邮轮弗吉尼亚号上,然后就被抛弃了。1900刚出生就成了孤儿,孤独的成长在弗吉尼亚号上,从未离开过这个摇晃的世界;也许是对他命运的补偿,上帝派可爱的小天使艾米丽照顾他。
可能是天使的点化,1900拥有不可思议的钢琴天赋,从未有人教,从没看过乐谱,但他却能凭着自己的感觉弹出最沁人心脾的旋律。当1900的音乐获得邮轮上所有人的欢迎时,他才8岁,而此时他已经乘着海轮往返欧美50多次了。
虽说是钢琴奇才,但1900还是个8岁的孩子,他有着和一般男孩一样的好奇的调皮,不过可能更有一层浪漫的色彩罢了:
这是一个风雨交加的夜晚,海风卷起层层巨浪拍打着弗吉尼亚号,邮轮随着巨浪剧烈的摇摆。船上的新萨克斯手迈克斯?托尼晕船了,1900将他 邀请到舞厅,然后——,然后松开了固定钢琴的闸,于是,钢琴随着海轮的倾斜滑动起来。准确的说,我们的主角1900、钢琴、邮轮随着1900的旋律一起跳 起了华尔兹,所有的事物好像都化为一体,随着“强弱弱”的节奏,托尼的晕船症也奇迹般地一点一点恢复。正如托尼在回忆录上这样写道:
大海摇晃着我们
使我们转来转去
快速的掠过灯和家具
我意识到我们正在和大海一起跳舞
真是完美而疯狂的舞者
晚上在金色的地板上快乐的跳着华尔兹是不是很惬意呢?也许,我们忘记了一个人,那就是艾米丽,她可没闲着:她必须在适当的时候施魔法帮助1900,不让钢琴碰上舞厅里的家具。而艾米丽还小,她无法施展魔法改变钢琴的运动方向或速度,而只能让钢琴停一下。
不妨认为舞厅是一个N行M列的矩阵,矩阵中的某些方格上堆放了一些家具,其他的则是空地。钢琴可以在空地上滑动,但不能撞上家具或滑出舞厅,否则会损坏钢琴和家具,引来难缠的船长。
每个时刻,钢琴都会随着船体倾斜的方向向相邻的方格滑动一格,其中相邻的方格可以是向东、向西、向南或向北的。而艾米丽可以选择施魔法或不施魔法,如果不施魔法,则钢琴会滑动,而如果施魔法,则钢琴会原地不动。 艾米丽是个天使,她知道每段时间的船体的倾斜情况。她想使钢琴尽量长时间在舞厅里滑行,这样1900会非常高兴,同时也有利于治疗托尼的晕船。但艾米丽还太小,不会算,所以希望你能帮助她。

【输入格式】

输入文件的第一行包含5个数N, M, x, y和K。N和M描述舞厅的大小,x和y为在第1时刻初钢琴的位置(x行y列);我们对船体倾斜情况是按时间的区间来描述的,比如“在[1, 3]时间里向东倾斜,[4, 5]时间里向北倾斜”,因此这里的K表示区间的数目。
以下N行,每行M个字符,描述舞厅里的家具。第i行第j列的字符若为‘ . ‘,则表示该位置是空地;若为‘ x ‘,则表示有家具。
以下K行,顺序描述K个时间区间,格式为:si ti di(1 ≤ i ≤ K)。表示在时间区间[si, ti]内,船体都是向di方向倾斜的。di为1, 2, 3, 4中的一个,依次表示北、南、西、东(分别对应矩阵中的上、下、左、右)。输入保证区间是连续的,即

s1 = 1
ti = si-1 + 1 (1 < i ≤ K)
tK = T
【输出格式】

输出文件仅有1行,包含一个整数,表示钢琴滑行的最长距离(即格子数)。

【输入样例】

4 5 4 1 3
..xx.
…..
…x.
…..
1 3 4
4 5 1
6 7 3
【输出样例】

6

显然我们可以枚举时间,行,列无脑do,当然——要超时.我们可以看见,在dp的是后当d==1即往上走,每一列就可用单调队列维护信息,时间由原来的枚举t,变到枚举k(时间段).

#include
#include
#include
using namespace std;
const int maxn=201;

int dx[4]={-1,1,0,0},dy[4]={0,0,-1,1};
int f[2][maxn][maxn],q[maxn],v[maxn];
char map[maxn][maxn];
int n,m,x,y,k,ans;
int cur1=1,cur2;
int a,b,c;

inline const int read(){
   register int x=0,f=1;
   register char ch=getchar();
   while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
   while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
   return x*f;
}

inline void solve(int x,int y,int w,int len,int c){
   register int i,h=1,t=0,k;
   for(i=1;i<=w;i++){
     if(map[x][y-1]=='.'){
       k=f[cur1][x][y]+w-i;
       while(h<=t&&k>v[t]) t--;
       v[++t]=k;
       q[t]=i;
       while(i-q[h]>len) h++;
       f[cur2][x][y]=v[h]-w+i;
     }
     else h=t+1,f[cur2][x][y]=-2e9;
     x=x+dx[c];
     y=y+dy[c];
   }
}

int main(){  
   register int i,j;
   n=read(),m=read(),x=read(),y=read(),k=read();
   for(i=1;i<=n;i++) scanf("%s",map[i]);
   for(i=1;i<=n;i++)
       for(j=1;j<=m;j++) f[0][i][j]=-2e9;
   f[0][x][y]=0;
   for(i=1;i<=k;i++){
       cur1=cur1^1;
       cur2=cur2^1;
       a=read(),b=read(),c=read(); 
       if(c==1) for(j=1;j<=m;j++) solve(n,j,n,b-a+1,c-1);  
       if(c==2) for(j=1;j<=m;j++) solve(1,j,n,b-a+1,c-1);  
       if(c==3) for(j=1;j<=n;j++) solve(j,m,m,b-a+1,c-1);  
       if(c==4) for(j=1;j<=n;j++) solve(j,1,m,b-a+1,c-1);   
   } 
   for(i=1;i<=n;i++)  
       for(j=1;j<=m;j++) ans=max(ans,f[cur2][i][j]);  
   printf("%d\n",ans); 
}

希望这篇博客能对大家有所帮助.

你可能感兴趣的:(单调队列/栈优化)