最近做了一些单调队列优化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
分析:对于 左边我们可以得到 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,k - 1) 相对于 dp[i][j] 来说 是 以知的 所以
右边同理 可得
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;
}