最近做了一些单调队列优化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)
韩父有N个儿子,分别是韩一,韩二…韩N。由于韩家演技功底深厚,加上他们间的密切配合,演出获得了巨大成功,票房甚至高达2000万。舟子是名很有威望的公知,可是他表面上两袖清风实则内心阴暗,看到韩家红红火火,嫉妒心遂起,便发微薄调侃韩二们站成一列时身高参差不齐。由于舟子的影响力,随口一句便会造成韩家的巨大损失,具体亏损是这样计算的,韩一,韩二…韩N站成一排,损失即为C*(韩i与韩i+1的高度差(1<=i<N))之和,搞不好连女儿都赔了.韩父苦苦思索,决定给韩子们内增高(注意韩子们变矮是不科学的只能增高或什么也不做),增高1cm是很容易的,可是增高10cm花费就很大了,对任意韩i,增高Hcm的花费是H^2.请你帮助韩父让韩家损失最小。
有若干组数据,一直处理到文件结束。 每组数据第一行为两个整数:韩子数量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
分析:对于 左边我们可以得到 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);
所以 原式变为
右边同理 可得
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; }