单调队列优化DP

 

最近做了一些单调队列优化DP的题目,现在总结一下。

遇到这类题目首先要能够想出朴素的DP状态转移方程,然后对其进行相应的转化,对于已知的部分用单调队列来维护。以下附几道题目,部分题解摘自别人的解题报告。(自己懒的写啦。其实是我觉得别人写的比我好T_T)

 

HDU 3401 Trade http://acm.hdu.edu.cn/showproblem.php?pid=3401

 

题意:给你T天股票的信息,每天的信息分别可以表示为:APi , BPi , ASi , BSi,分别表示第i天时股票的买入单价、卖出单价、第i天最多可以买入的股票数、最多可以卖出的股票数,并且还有一个约束条件,那就是两个交易日相距的天数要大于W天,问最终最多能

获利多少。

分析:

朴素的状态转移方程:

购买 :dp[i][j] = max {dp[pre][k] - (j - k) * b[i] }; 其中 0 < j - k < bnum[i]  (pre代表j之前某天)
卖出 :dp[i][j] = max {dp[pre][k] + (k - j) * s[i] }; 其中 0 < k - j < snum[i]
不买也不卖:dp[i][j] = max {dp[i][j], dp[i - 1][j] };

时间复杂度为:O( maxP ^ 2 * T ^ 2)显然要超时。

对于购买状态转移方程可以转化为:dp[i][j]=max(dp[pre][k]+k*b[i]-j*b[i]),对于dp[pre][k]+k*b[i]已确定的情况可以用单调对列来处理。对于卖出的情况同理。

 

Source Code:

#include <iostream>
#include<cstdio>
using namespace std;
#define min(a,b) ((a)<(b)?(a):(b))
#define max(a,b) ((a)>(b)?(a):(b))

const int inf=0x3f3f3f3f;
const int maxn=2010;
int dp[maxn][maxn],bp[maxn],sp[maxn],bnum[maxn],snum[maxn];
struct node{
    int mon,num;
}q[maxn];

int main()
{
   int t,n,m,w,k,i,j;
   scanf("%d",&t);
   while(t--){
    scanf("%d %d %d",&n,&m,&w);
    for(i=1;i<=n;i++)
        scanf("%d %d %d %d",&bp[i],&sp[i],&bnum[i],&snum[i]);
    for(i=0;i<=n;i++){//初始化
        for(j=0;j<=m;j++)
            dp[i][j]=-inf;
    }
    for(i=1;i<=m;i++){//预处理
        dp[i][0]=0;
        for(j=1;j<=bnum[i];j++)
            dp[i][j]=-j*bp[i];
    }
    for(i=2;i<=n;i++){//DP
        for(j=0;j<=m;j++)//不买也不卖
            dp[i][j]=max(dp[i][j],dp[i-1][j]);
        if(i<w+2) continue;
        int pre=i-w-1;
        int l=0,r=0;
        for(j=0;j<=m;j++){
            while(l<r&&q[r-1].mon-(j-q[r-1].num)*bp[i]<dp[pre][j]) r--;
            //若买第i天的股票达到j个股票的盈利小于第pre天j个股票的盈利,则剔除队尾

            q[r].num=j;
            q[r++].mon=dp[pre][j];
            while(l<r&&q[l].num+bnum[i]<j) l++;
            //若买下第i天最多能买的股票还是达不到j个,则剔除队首
            if(l<r) dp[i][j]=max(dp[i][j],q[l].mon-(j-q[l].num)*bp[i]);
        }
        l=r=0;
        for(j=m;j>=0;j--){
            while(l<r&&q[r-1].mon+(q[r-1].num-j)*sp[i]<dp[pre][j]) r--;
            //若第i天卖股票达到j个的盈利小于第pre天j个股票的盈利,则剔除队尾
            q[r].num=j;
            q[r++].mon=dp[pre][j];
            while(l<r&&q[l].num-snum[i]>j) l++;
            //若卖掉第i天最多能卖的股票还是达不到j个,则剔除对首
            if(l<r) dp[i][j]=max(dp[i][j],q[l].mon+(q[l].num-j)*sp[i]);
        }
    }
    int ans=0;
    for(j=0;j<=m;j++)
        ans=max(ans,dp[n][j]);
    printf("%d\n",ans);
   }
   return 0;
}


 

UESTC 1685 我要长高 http://www.acm.uestc.edu.cn/problem.php?pid=1685

(此题转自:http://www.cnblogs.com/ka200812/archive/2012/07/11/2585950.html

 

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[i][j]表示第 i 个儿子身高为 j 的最低花费。分析题目很容易知道,当前儿子的身高花费只由前一个儿子影响。因此,

dp[i][j]=min(dp[i-1][k] + abs(j-k)*C + (x[i]-j)*(x[i]-j));其中x[i]是第i个儿子原本的身高

我们分析一下复杂度。

首先有N个儿子,这需要一个循环。再者,每个儿子有0到100的身高,这也需要一维。再再者,0到100的每一个身高都可以有前一位儿子的身高0到100递推而来。

所以朴素算法的时间复杂度是O(n^3)。题目只给两秒,难以接受!

分析方程:

当第 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[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]。转化成这样的形式,我们就可以用单调队列进行优化了。

第二个方程同理。

 

Source Code:

 #include<iostream>
 #include<string>
 #include<stdio.h>
 #include<memory.h>
 using namespace std;
 #define inf 0xfffffff
 #define min(a,b) a<b?a:b
 #define max(a,b) a>b?a:b
 
 int dp[2][101];
 int n,c;
 int q[101];
 int head,tail,cur;
 
 int main()
 {
     int i,j,x,nowf;
     freopen("D:\\in.txt","r",stdin);
     while(scanf("%d%d",&n,&c)==2)
     {
         scanf("%d",&x);
         cur=0;
         for(i=0;i<x;i++)
             dp[cur][i]=inf;
         for(i=x;i<=100;i++)
             dp[cur][i]=(x-i)*(x-i);
         for(i=1;i<n;i++)
         {
             scanf("%d",&x);
             cur=1-cur;
             //比前一个人高
             head=tail=0;
             for(j=0;j<=100;j++) //当身高为j时候,队列里便已经保存了0~j-1的信息,注意,是第i-1个人的信息
             {
                 nowf=dp[1-cur][j]-j*c;
                 while(head<tail && q[tail-1]>nowf)
                     tail--;
                 q[tail++]=nowf;
                 if(j<x)
                     dp[cur][j]=inf;    
                 else
                     dp[cur][j]=q[head]+j*c+(x-j)*(x-j);
             }
             //比前一个人矮
             head=tail=0; 
             for(j=100;j>=0;j--) //当身高为j时候,队列里便已经保存了100~j+1的信息,正写反写是有技巧的
             {
                 nowf=dp[1-cur][j]+j*c;
                 while(head<tail && q[tail-1]>nowf)
                     tail--;
                 q[tail++]=nowf;
                 if(j>=x)
                     dp[cur][j]=min(dp[cur][j],q[head]-j*c+(x-j)*(x-j));
             }
         }
         int ans=inf;
         for(i=0;i<=100;i++)
             ans=min(ans,dp[cur][i]);
         printf("%d\n",ans);
     }
     return 0;
 }

 


HDU 4374 One hundred layer http://acm.hdu.edu.cn/showproblem.php?pid=4374

题意: 有n层,每层有m个part。在一层中你只能向着一个方向移动(左或者右),最多能移动T步, 经过每个部分是都能得到这个部分的分数。起始位置在x位置,从第一层到最顶层能得到最多的分数

分析:对于 左边我们可以得到  dp[i][j] = max( dp[i - 1][k]  + sum(k, j))       j - t <=k <= j;

我们的队列 要维护的 就是 dp[i - 1][k]  + sum(k, j)

由于 sum (k ,j) 单调队列要维护的状态必须是和 现在的状态 无关(或着说是 以知的),但 sum(k,j)  用到了现在的状态;

 所以 我们要进行转换一下,   sum(k,j) = sum (1,j) - sum (1,k - 1);

所以 原式变为    

dp[i - 1][k]  + sum (1,j) - sum (1,k - 1)   = (dp[i - 1][k]   - sum(1,k))+ sum (1,j) 
而 dp[i -1][k] - sum(1,k - 1)  相对于 dp[i][j]  来说 是 以知的   所以   
dp[i -1][k] - sum(1,k - 1)      就是  我们 队列 要维护的状态

右边同理 可得

Source Cede:

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;

const int inf=0x3f3f3f3f;
int dp[105][10010],sum[10010],num[105][10010];
int n,m,x,t;
struct node{
    int idx,sc;
}q[10010];

int main()
{
    //freopen("D:\in.txt","r",stdin);
    while(scanf("%d %d %d %d",&n,&m,&x,&t)==4){
        for(int i=1;i<=n;i++){
            for(int j=1;j<=m;j++)
                scanf("%d",&num[i][j]);
        }
        memset(dp,-inf,sizeof(dp));
        dp[0][x]=0;
        sum[0]=0;
        for(int i=1;i<=n;i++){
            int l=0,r=0;
            for(int j=1;j<=m;j++){
                sum[j]=num[i][j]+sum[j-1];
                while(l<r&&dp[i-1][j]-sum[j-1]>q[r-1].sc) r--;
                q[r].idx=j;
                q[r++].sc=dp[i-1][j]-sum[j-1];
                while(l<r&&j-q[l].idx>t) l++;
                dp[i][j]=max(dp[i][j],q[l].sc+sum[j]);
            }
            l=r=0;
            for(int j=m;j>0;j--){
                while(l<r&&dp[i-1][j]+sum[j]>q[r-1].sc) r--;
                q[r].idx=j;
                q[r++].sc=dp[i-1][j]+sum[j];
                while(l<r&&q[l].idx-j>t) l++;
                dp[i][j]=max(dp[i][j],q[l].sc-sum[j-1]);
            }
        }
        int ans=-inf;
        for(int i=1;i<=m;i++)
            ans=max(ans,dp[n][i]);
        printf("%d\n",ans);
    }

    return 0;
}


 

 POJ 1821 Fence http://poj.org/problem?id=1821

题意:N面墙,K个人来粉刷,每个人有一个粉刷数量的限制,且各个人粉刷一面墙的价格不同,每个人的初始位置在某面墙前,且该面墙或要由他来刷,或由其他的人刷,或不刷,问粉刷这N面墙可以得到的最大的价值。

分析:很容易可以想到该题的状态转移方程:

dp[i][j]= max {   dp[i-1][j](不用该工人),

                         dp[i][j-1] (不刷这面墙),

                         dp[i - 1][k] + men[i].price*(j - k)  从第k+1面墙刷到第j面 且k >= j - l; 

}(dp[i][j]表示有前i个人刷到第j面墙可以得到的最大价值)。

对于dp[i-1][k]+men[i].price*(j-k),转化成dp[i-1][k]-k*men[i].price+j*men[i].price,对前i-1个人的最大价值进行单调队列优化。

(这题虽然我很快的想出了状态转移方程,但对于细节的处理还是很搓,一直不对,所以这里的代码参考了:http://hi.baidu.com/yimaolionel/item/4b1155f0b2f23b1fd7ff8c36)

 

Source Code:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#define max(a,b) ((a)>(b)?(a):(b))
using namespace std;

const int maxn=16010;
int dp[110][maxn];
struct node1{
    int idx,mon;
}q[maxn];
struct node2{
    int lim,pos,pri;
    bool operator <(node2 a)const{
        return pos<a.pos;
    }
    void Init(){
        scanf("%d %d %d",&lim,&pri,&pos);
    }
}men[110];

int main()
{
    //freopen("D:\in.txt","r",stdin);
    int n,k;
    while(scanf("%d %d",&n,&k)==2){
        for(int i=1;i<=k;i++)
            men[i].Init();
        sort(men+1,men+k+1);
        memset(dp,0,sizeof(dp));
        int i,j;
        for(i=1;i<=k;i++){
            for(j=0;j<men[i].pos;j++)
                dp[i][j]=dp[i-1][j];
            int head=0,tail=0;
            for(j=max(0,men[i].pos-men[i].lim);j<men[i].pos;j++){
                while(head<tail&&q[tail-1].mon<(dp[i-1][j]-j*men[i].pri))tail--;
                q[tail].idx=j;
                q[tail++].mon=dp[i-1][j]-men[i].pri*j;
            }
            for(j=men[i].pos;j<(men[i].pos+men[i].lim);j++){
                while(head<tail&&(j-q[head].idx>men[i].lim)) head++;
                dp[i][j]=max(max(dp[i][j-1],dp[i-1][j]),q[head].mon+j*men[i].pri);
            }
            for(;j<=n;j++)
                dp[i][j]=max(dp[i][j-1],dp[i-1][j]);
        }
        printf("%d\n",dp[k][n]);
    }
    return 0;
}


 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
单调队列优化DP是一种常用的优化方法,可以将时间复杂度从 $O(n^2)$ 降低到 $O(n)$ 或者 $O(n \log n)$。以下是一道利用单调队列优化DP的典型题目: 题目描述: 给定一个长度为 $n$ 的序列 $a_i$,定义 $f(i)$ 为 $a_i$ 到 $a_n$ 中的最小值,即 $f(i) = \min\limits_{j=i}^n a_j$。现在定义 $g(i)$ 为满足 $f(j) \ge a_i$ 的最小下标 $j$,即 $g(i) = \min\{j \mid j > i, f(j) \ge a_i\}$。如果不存在这样的下标 $j$,则 $g(i) = n+1$。 现在请你计算出 $1 \le i \le n$ 的所有 $g(i)$ 的值。 输入格式: 第一行包含一个整数 $n$。 第二行包含 $n$ 个整数 $a_1,a_2,\cdots,a_n$。 输出格式: 输出 $n$ 行,第 $i$ 行输出 $g(i)$ 的值。 输入样例: 5 3 1 2 4 5 输出样例: 2 5 5 5 6 解题思路: 设 $dp(i)$ 表示 $g(i)$,那么 $dp(i)$ 与 $dp(i+1)$ 的转移关系可以表示为: $$dp(i)=\begin{cases}i+1, &\text{if}\ f(i+1)\ge a_i \\dp(i+1), &\text{else}\end{cases}$$ 这个转移方程可以使用暴力 DP 解决,时间复杂度为 $O(n^2)$。但是,我们可以使用单调队列优化 DP,将时间复杂度降为 $O(n)$。 我们定义一个单调队列 $q$,存储下标。队列 $q$ 中的元素满足: - 队列中的元素是单调递减的,即 $q_1 < q_2 < \cdots < q_k$; - 对于任意的 $i\in [1,k]$,有 $f(q_i) \ge f(q_{i+1})$。 队列 $q$ 的作用是维护一个长度为 $k$ 的区间 $[i+1,q_k]$,满足这个区间中的所有 $j$ 都满足 $f(j) < f(i+1)$。 根据定义,当我们要求 $dp(i)$ 时,只需要查找队列 $q$ 中第一个满足 $f(q_j) \ge a_i$ 的位置 $q_j$,那么 $g(i) = q_j$,如果队列 $q$ 中不存在这样的位置,则 $g(i) = n+1$。 那么如何维护单调队列 $q$ 呢?我们可以在每次 DP 的过程中,将 $i$ 加入队尾。然后判断队首元素 $q_1$ 是否满足 $f(q_1) \ge a_i$,如果满足则弹出队首元素,直到队首元素不满足条件为止。 由于每个元素最多被加入队列一次,并且最多被弹出一次,因此时间复杂度为 $O(n)$。具体实现细节可以参考下面的代码实现:
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值