单调队列的定义:一种严格单调的队列(严格递增或者递减),其中队列中元素的位置也有一定的递增递减的性质。队首位置保存的是最优解,接着次优解。。
单调队列的维护(假设队列为严格递增队列):
1.新插入一个数到队尾(此时还没有插入),比较当前队尾的数和当前插入的数,如果当前插入的数小于队尾的数,弹出队尾的数。。。继续比较队尾的数和当前插入的数,直到队尾的数小于当前插入的数为止,在队尾插入当前的数
2.更新队首的数。队首的数是最优解,但是要保证队尾和队首的数在原数组中的距离不大于题意中的K(即要求最优解在当前枚举点的前K的区间内),如不符合则弹出队首,直到队首满足为止。
单调队列模板题:
poj2823
题意:求出i到i+k的所有区间中的最小和最大值,其中1 <= i <= n - k + 1。
#include<cstdio>
#include<cstring>
#define mem(a,b) memset(a,b,sizeof(a))
#define For(a,b,c) for(int a = b;a <= c;a++)
using namespace std;
typedef long long ll;
const int maxn = 1000005;
const double esp = 1e-5;
const ll mod = 1e9 + 7;
int n,k;
int ori[maxn];
int pos[maxn* 2];
int ans[maxn];
int que[maxn * 2];
int main()
{
while(~scanf("%d%d",&n,&k))
{
for(int i = 1;i <= n;i++)scanf("%d",&ori[i]);
int l = 1,r = 0;
for(int i = 1;i <= k - 1;i++)
{
while(l <= r && que[r] >= ori[i])
r--;
que[++r] = ori[i];
pos[r] = i;
}
for(int i = k;i <= n;i++)
{
while(l <= r && que[r] >= ori[i])
r--;
que[++r] = ori[i];
pos[r] = i;
while(pos[l] < i - k + 1)
l++;
printf(i == n ?"%d\n":"%d ",que[l]);
}
r = 0 , l = 1;
for(int i = 1;i <= k - 1;i++)
{
while(l <= r && que[r] <= ori[i])
r--;
que[++r] = ori[i];
pos[r] = i;
}
for(int i = k;i <= n;i++)
{
while(l <= r && que[r] <= ori[i])
r--;
que[++r] = ori[i];
pos[r] = i;
while(pos[l] < i - k + 1)
l++;
printf(i == n ? "%d\n" : "%d ",que[l]);
}
}
return 0;
}
单调队列最主要的用法还是用来优化dp,它能将类似dp[i] = max/min(f[k]) + g[i]的dp降一个维度。优化的对象就是f[k] (k < i && g[i]是与k无关的量)
接下来来几个单调队列优化dp的例题:
http://acm.uestc.edu.cn/#/problem/show/594 我要长高
容易想到状态为dp[i][j],表示第i个人身高是j的最小费用
转移方程也比较容易想到,dp[i][j] = min(dp[i][j],dp[i - 1][k] + abs(j - k) * C + (j - ori[i]) ^ 2)
其中1<=k<=j,1<=j<=100,ori[i]为第i个人的原始身高
如果只是普通dp的话肯定会超时,因为要枚举每个人,枚举当前人的身高,枚举前一个人的身高,复杂度为O(n * h * h)
如果用单调队列优化,队列中已经存放了第i - 1的1~j-1的身高,所以枚举第i-1的k个身高就免了,复杂度降为O(n * h)
另外此题当前人只跟前面一个人的身高有关,所以可以用滚动数组
上面的状态转移方程要分j >= k和j < k考虑
j >= k时,dp[i][j] = min(dp[i][j],dp[i - 1][k] - k * C + j * C + (j - ori[i]) ^ 2)
j < k时,dp[i][j] = min(dp[i][j],dp[i - 1][k] + k * C - j * C + (j - ori[i]) ^ 2)
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define For(a,b,c) for(int a = b;a <= c;a++)
using namespace std;
typedef long long ll;
const int maxn = 50005;
const double esp = 1e-5;
const ll mod = 1e9 + 7;
const int INF = 0x3f3f3f3f;
int ori[maxn];
int que[maxn * 5];
int n,c;
int dp[maxn][102];
int main()
{
while(~scanf("%d%d",&n,&c))
{
for(int i = 1;i <= n;i++)scanf("%d",ori + i);
int now = 0,pre;
for(int i = 1;i <= 100;i++)//初始化第一个人的费用
{
if(i < ori[1])dp[now][i] = INF;//变矮是不可能的,所以要设置无限大
else dp[now][i] = (i - ori[1]) * (i - ori[1]);
}
int l,r;
int val;
for(int i = 2;i <= n;i++)
{
pre = now,now = 1 - now;//由于当前人的费用只由前一个人的身高决定,所以可以用滚动数组
l = 0,r = 0;
for(int j = 1;j <= 100;j++)//从1~100,枚举到j时,说明第i-1个人的前1~j-1的身高信息都在队列中了
{
val = dp[pre][j] - j * c;
while(l < r && que[r - 1] >= val)
r--;
que[r++] = val;
if(j < ori[i])//小于之前的身高是不可能的,下面同理
dp[now][j] = INF;
else dp[now][j] = que[l] + j * c + (j - ori[i]) * (j - ori[i]);
}
l = 0,r = 0;
for(int j = 100;j >= 1;j--)
{
val = dp[pre][j] + j * c;
while(l < r && que[r - 1] >= val)
r--;
que[r++] = val;
if(j < ori[i])
dp[now][j] = INF;
else dp[now][j] = min(dp[now][j],que[l] - j * c + (j - ori[i]) * (j - ori[i]));
}
}
int ans = INF;
for(int i = 1;i <= 100;i++)
ans = min(ans,dp[now][i]);
cout<<ans<<endl;
}
}
hdu3401:http://acm.hdu.edu.cn/showproblem.php?pid=3401
题意:某个人一开始有无限的钱,在第i天能以api的价格买如一个股票,以bpi的价格卖出一个股票,且当天最多能买asi个股票,最多能卖bsi个股票,这个人最多能持有maxp个股票。若一个人在第i天买入或者卖出股票,则最快只能在i + w + 1天再次买如或者卖出股票,问最多能能赚多少钱。
思路:状态转移方程容易列出 dp[i][j]表示第i天持有j个股票
dp[i][j] = max(dp[i - 1][j],dp[i][j]) 第i-1天不买不卖到第i天
dp[i][j] = max(dp[i - 1 - w][k] - (j - k) * ap[i],dp[i][j]),第i-1-w天买如股票,可以化简为
dp[i][j] = max(dp[i - 1 - w][k] + k * ap[i] - j * ap[i],dp[i][j])
dp[i][j] = max(dp[i - 1 - w][k] - (k - j) * bp[i],dp[i][j]),第i-1-w天卖出股票,可以化简为
dp[i][j] = max(dp[i - 1 - w][k] - k * bp[i] + j * bp[i],dp[i][j])
由上面的式子可以看出可以用单调队列优化降维
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define For(a,b,c) for(int a = b;a <= c;a++)
using namespace std;
typedef long long ll;
const int maxn = 2005;
const double esp = 1e-5;
const ll mod = 1e9 + 7;
const int INF = 999999999;
int t;
int T,maxp,W;
struct ppp
{
int ap,bp,as,bs;
void read()
{
scanf("%d%d%d%d",&ap,&bp,&as,&bs);
}
}d[maxn];
int que[maxn * 1000];
int pos[maxn * 1000];
int dp[maxn][maxn];
int main()
{
scanf("%d",&t);
while(t--)
{
scanf("%d%d%d",&T,&maxp,&W);
for(int i = 1;i <= T;i++)
d[i].read();
for(int i = 0;i <= T;i++)
for(int j = 0;j <= maxp;j++)
dp[i][j] = -INF;//先全部置为不可能的情况
for(int i = 1;i <= W + 1;i++)
for(int j= 0;j <= min(d[i].as,maxp);j++)
dp[i][j] = - j * d[i].ap;//前w+1天可由第0天推出来,(其实第0天不存在)
int now;
for(int i = 1;i <= T;i++)
{
for(int j = 0;j <= maxp;j++)//当天的情况可由昨天推过来
dp[i][j] = max(dp[i - 1][j],dp[i][j]);
if(i <= W + 1)continue;
int pre = i - W - 1;
int l = 0,r = 0;
for(int j = 0;j <= maxp;j++)
{
now = dp[pre][j] + j * d[i].ap;
while(l < r && que[r - 1] < now)
r--;
que[r] = now;pos[r++] = j;
while(l < r && pos[l] + d[i].as < j)//队首元素持有的股票加上当天能买的股票比枚举的还要少,说明队首元素已经不可能推到当天
l++;
dp[i][j] = max(dp[i][j],que[l] - j * d[i].ap);
}
l = 0,r = 0;
for(int j = maxp;j >= 0;j--)
{
now = dp[pre][j] + j * d[i].bp;
while(l < r && que[r - 1] < now)
r--;
que[r] = now;pos[r++] = j;
while(l < r && pos[l] - d[i].bs > j)
l++;
dp[i][j] = max(dp[i][j],que[l] - j * d[i].bp);
}
}
int ans = -INF;
for(int i = 0;i <= maxp;i++)
ans = max(ans,dp[T][i]);
cout<<ans<<endl;
}
}
poj1821:http://poj.org/problem?id=1821
题意:有k个工人,在si的方格上,他能连续地涂Li个格子(第si个方格必须包括),每涂一个能赚pi元,求涂方格赚取的最大值。
思路:状态方程dp[i][j],表示第i个工人涂到第j个方格赚取的最大利益,那么有状态转移方程
dp[i][j] = max(dp[i][j],dp[i - 1][j])到第j个i都不涂,由工人i-1涂到第j个来推出最大利益
dp[i][j] = max(dp[i][j - 1],dp[i][j])涂到第j个右涂到第j-1个推出最大利益
dp[i][j] = max(dp[i][j],dp[i - 1][k] + (j - k) * p[i])第i-1个工人只涂到第k个,第k + 1到第j个由i来涂,显然这个方程不用单调队列优化的是O(k * N * N),优化后可降维成O(k * N * N)
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<iostream>
#include<cstring>
#define mem(a,b) memset(a,b,sizeof(a))
#define For(a,b,c) for(int a = b;a <= c;a++)
using namespace std;
typedef long long ll;
const int maxn = 16010;
const double esp = 1e-5;
const int INF = 999999999;
int dp[105][maxn];
int n,k;
struct ppp
{
int L,p,s,l,r;
void read()
{
scanf("%d%d%d",&L,&p,&s);
}
void make()
{
l = max(1,s - L + 1);//计算左边界,当前油漆工向左所能涂到的最远的格子
r = min(n,s + L - 1);//计算右边界,当前油漆工向右所能涂到的最远的格子
}
bool operator <(const ppp& b)const
{
return s < b.s;
}
}p[105];
int que[maxn * 10];
int pos[maxn * 10];
int main()
{
while(~scanf("%d%d",&n,&k))
{
for(int i = 1 ;i <= k;i++)p[i].read();
sort(p + 1,p + 1 + k);
for(int i= 1;i <= k;i++)p[i].make();
for(int i = 0;i <= n;i++)dp[0][i] = 0;//初始化边界
int l,r;
int temp;
for(int i = 1;i <= k;i++)
{
l = 0,r = 0;
for(int j = 0;j <= n;j++)dp[i][j] = dp[i - 1][j];//就算当前油漆工什么都不干,也能右上一个油漆工得到当前值
for(int j = p[i].l - 1;j < p[i].s;j++)//将i-1个油漆工涂到k的情况加入单调队列
{
temp = dp[i - 1][j] - j * p[i].p;
while(l < r && que[r - 1] <= temp)
r--;
que[r] = temp;pos[r] = j;r++;
}
for(int j = p[i].s;j <= p[i].r;j++)//取出最优情况
{
while(l < r && j - pos[l] > p[i].L)
l++;
dp[i][j] = max(dp[i][j],dp[i][j - 1]);//涂到第j的最大值可以由到j-1涂,第j个不涂推出
dp[i][j] = max(dp[i][j],que[l] + j * p[i].p);
}
for(int j = p[i].r + 1; j <= n;j++)dp[i][j] = max(dp[i][j - 1],dp[i - 1][j]);//后面一些不可涂到的地方可由上一个工人的涂到j的最大值和当前工人涂到j-1的最大值推出
}
int ans = 0;
for(int i = 1;i <= n;i++)
ans = max(ans,dp[k][i]);
printf("%d\n",ans);
}
}