最近几天接触了单调队列,还接触了单调栈,就总结一下。
其实单调队列,和单调栈都是差不多的数据类型,顾名思义就是在栈和队列上加上单调,单调递增或者单调递减。当要入栈或者入队的时候,要和栈头或者队尾进行比较,满足单调的性质则入队入栈,否则将当前元素删去,直到满足单调性质。
那么问题来了,单调队列,和单调栈有什么用了。最普遍的最重要的作用就是起到优化的作用。当然我目前也只知道这个所用。
先看一道例题:
求一个n序列中,所有长度不大于lmaxin,的连续子序列中,序列和最大的是多少。最容易想到的方法,效率是O(n*lmaxin);
for(int i=0;i<n;i++)
{
int s=0;
for(int j=i;j>=max(0,i-lmaxin+1);j--)
{
s+=a[i];
ans=max(ans,s);
}
}
上面的代码中其实有很多都是重复比较了,所以效率低
单调队列优化的代码
rear=head=0;
for(int i=1;i<=n;i++)
{
while(rear>=front&&s[i]<q[rear-1])
{rear--;}
q[rear++]=i;
while(q[front]<i-m)
front++;
ans=max(ans,s[i]-s[q[front]]);
}
s数组是前缀和,给定一个端点,求端点左边长度为m的区间的最小值就可以了。其实就相当于把原来的长度为m的for循环变成单调队列。区间长度是固定的m,单调队列求解区间的最小值,比暴力的for循环要高效率的多,因为元素只是一进一出,效率是O(n)。那么联系到单调栈,那么单调栈可以优化区间长度不断增加且左端点固定的最大值(个人联想)。
那么经常听到的单调队列优化背包的也很好理解了。给一道题目多重背包单调队列优化,hdu上面不用优化,poj需要优化
http://poj.org/problem?id=1742
hdu AC的代码
#include <iostream>
#include <string.h>
#include <math.h>
#include <algorithm>
#include <stdlib.h>
using namespace std;
#define MIN -9999
int dp[100005];
int n;
int m;
int a[100005];
int b[1005];
void ZeroOnePack(int v,int w)
{
for(int i=m;i>=w;i--)
{
if(dp[i-w]!=MIN)
dp[i]=max(dp[i],dp[i-w]+v);
}
}
void CompletePack(int v,int w)
{
for(int i=w;i<=m;i++)
{
if(dp[i-w]!=MIN)
dp[i]=max(dp[i],dp[i-w]+v);
}
}
void MultiplyPack(int v,int w,int c)
{
if(c*w>m)
{
CompletePack(v,w);
return ;
}
int k;
k=1;
while(k<c)
{
ZeroOnePack(k*v,k*w);
c=c-k;
k<<=1;
}
ZeroOnePack(c*v,c*w);
}
int main()
{
int result;
while(scanf("%d%d",&n,&m)!=EOF)
{
result=0;
if(n==0&&m==0)
break;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
scanf("%d",&b[i]);
for(int i=0;i<=m;i++)
dp[i]=MIN;
dp[0]=0;
for(int i=1;i<=n;i++)
MultiplyPack(a[i],a[i],b[i]);
for(int i=1;i<=m;i++)
{
if(dp[i]!=MIN)
result++;
}
printf("%d\n",result);
}
return 0;
}
poj 单调队列优化的POJ的代码
#include <iostream>
#include <string.h>
#include <stdlib.h>
#include <algorithm>
#include <math.h>
using namespace std;
#define MAX 100000
int dp[MAX+5];
int a[105];
int c[105];
int n,m;
int rear;
int front;
int queue[MAX+5];
int ans;
int main()
{
while(scanf("%d%d",&n,&m)!=EOF)
{
if(n==0&&m==0)
break;
ans=0;
for(int i=0;i<n;i++)
scanf("%d",&a[i]);
for(int j=0;j<n;j++)
scanf("%d",&c[j]);
memset(dp,0,sizeof(dp));
dp[0]=1;
for(int i=0;i<n;i++)
{
if(c[i]==1)
{
for(int j=m;j>=a[i];j--)
if(!dp[j]&&dp[j-a[i]])
dp[j]=1;
}
else if(c[i]*a[i]>=m)
{
for(int j=a[i];j<=m;j++)
if(!dp[j]&&dp[j-a[i]])
dp[j]=1;
}
else
{
for(int k=0;k<a[i];k++)
{
rear=0;front=0;int sum=0;
for(int j=k;j<=m;j+=a[i])
{
if((rear-front)==c[i]+1)
sum-=queue[front++];
queue[rear++]=dp[j];
sum+=dp[j];
if(!dp[j]&&sum)
dp[j]=1;
}
}
}
}
for(int i=1;i<=m;i++)
if(dp[i]) ans++;
printf("%d\n",ans);
}
}
第一个是倍增方法,第二个是单调队列优化的方法,其实不用分成0 1背包和完全背包,直接用单调队列就可以。多重背包优化,要把根据余数把体积分成几类
参考这个资料的
之间用倍增法写的多重背包都可以用单调队列优化
http://acm.hdu.edu.cn/showproblem.php?pid=2191
可以试试用单调队列进行优化
//
// main.cpp
// 单调队列优化1
//
// Created by 陈永康 on 16/1/16.
// Copyright © 2016年 陈永康. All rights reserved.
//
#include <iostream>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <algorithm>
using namespace std;
int c[105];
int w[105];
int v[105];
int dp[105];
int n,m;
int b[105];
int a[105];
int rear,front;
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
memset(dp,0,sizeof(dp));
scanf("%d%d",&m,&n);
for(int i=0;i<n;i++)
scanf("%d%d%d",&w[i],&v[i],&c[i]);
for(int i=0;i<n;i++)
{
for(int j=0;j<w[i];j++)
{
rear=front=0;
for(int k=0;k<=(m-j)/w[i];k++)
{
int x=k;
int y=dp[k*w[i]+j]-k*v[i];
while(front<rear&&y>=b[rear-1])
rear--;
a[rear]=x;
b[rear++]=y;
while(a[front]<k-c[i])
front++;
dp[k*w[i]+j]=b[front]+k*v[i];
}
}
}
printf("%d\n",dp[m]);
}
return 0;
}
单调队列不仅可以优化多重背包,可以优化别的DP问题
一道好题目
这是时间超限的代码
//
// main.cpp
// 单调队列优化3
//
// Created by 陈永康 on 16/1/19.
// Copyright © 2016年 陈永康. All rights reserved.
//
#include <iostream>
#include <string.h>
#include <math.h>
#include <stdlib.h>
#include <algorithm>
using namespace std;
#define MAX 1<<30
int dp[2005][2005];
int w,t,MaxP;
int buy[2005];
int sell[2005];
int MaxB[2005];
int MaxS[2005];
int main()
{
int cas;
scanf("%d",&cas);
while(cas--)
{
scanf("%d%d%d",&t,&MaxP,&w);
for(int i=1;i<=t;i++)
scanf("%d%d%d%d",&buy[i],&sell[i],&MaxB[i],&MaxS[i]);
for(int i=1;i<=2000;i++)
for(int j=1;j<=2000;j++)
dp[i][j]=-MAX;
for(int i=0;i<=MaxB[1];i++)
dp[1][i]=0-buy[1]*i;
for(int i=2;i<=t;i++)
{
for(int j=0;j<=MaxP;j++)
{
for(int k=1;k<=MaxP;k++)
{
if(i-w-1<=0||j-k>MaxB[i]||j<=k)
continue;
if(dp[i-w-1][k]==MAX)
continue;
dp[i][j]=max(dp[i][j],dp[i-w-1][k]-buy[i]*(j-k));
}
for(int k=1;k<=MaxP;k++)
{
if(i-w-1<=0||k<=j||k-j>MaxS[i])
continue;
if(dp[i-w-1][k]==MAX)
continue;
dp[i][j]=max(dp[i][j],dp[i-w-1][k]+sell[i]*(k-j));
}
dp[i][j]=max(dp[i][j],dp[i-1][j]);
}
}
int ans=0;
for(int j=0;j<=MaxP;j++)
ans=max(ans,dp[t][j]);
printf("%d\n",ans);
}
return 0;
}
状态转移方程不难想到,但是写出来就是时间超限的,用单调队列优化的代码
//
// main.cpp
// 单调队列优化3
//
// Created by 陈永康 on 16/1/19.
// Copyright © 2016年 陈永康. All rights reserved.
//
#include <iostream>
#include <string.h>
#include <math.h>
#include <stdlib.h>
#include <algorithm>
using namespace std;
#define MAX 1<<30
int dp[2005][2005];
int w,t,MaxP;
int buy[2005];
int sell[2005];
int MaxB[2005];
int MaxS[2005];
int a[2005];
int b[2005];
int rear;
int front;
int main()
{
int cas;
scanf("%d",&cas);
while(cas--)
{
scanf("%d%d%d",&t,&MaxP,&w);
for(int i=1;i<=t;i++)
scanf("%d%d%d%d",&buy[i],&sell[i],&MaxB[i],&MaxS[i]);
for(int i=1;i<=2000;i++)
for(int j=1;j<=2000;j++)
dp[i][j]=-MAX;
for(int i=1;i<=t;i++)
for(int j=0;j<=min(MaxP,MaxB[i]);j++)
dp[i][j]=0-buy[i]*j;
for(int i=2;i<=t;i++)
{
for(int j=0;j<=MaxP;j++)
dp[i][j]=max(dp[i][j],dp[i-1][j]);
if(i-w-1<=0)
continue;
rear=front=0;
b[rear]=dp[i-w-1][0];
a[rear]=0;
for(int j=1;j<=MaxP;j++)
{
int x=j;
int y=dp[i-w-1][j];
while(front<=rear&&b[rear]-(j-a[rear])*buy[i]<y)
rear--;
b[++rear]=y;
a[rear]=x;
while(front<=rear&&a[front]+MaxB[i]<j)
front++;
dp[i][j]=max(dp[i][j],b[front]-(j-a[front])*buy[i]);
}
rear=front=0;
b[rear]=dp[i-w-1][MaxP];
a[rear]=MaxP;
for(int j=MaxP-1;j>=0;j--)
{
int x=j;
int y=dp[i-w-1][j];
while(front<=rear&&b[rear]+(a[rear]-j)*sell[i]<y)
rear--;
b[++rear]=y;
a[rear]=x;
while(front<=rear&&a[front]-MaxS[i]>j)
front++;
dp[i][j]=max(dp[i][j],b[front]+(a[front]-j)*sell[i]);
}
}
int ans=0;
for(int j=0;j<=MaxP;j++)
ans=max(ans,dp[t][j]);
printf("%d\n",ans);
}
return 0;
}
说了这么多单调队列的,就说一下单调栈的应用
http://poj.org/problem?id=2082
题目的意思就是给你一系列矩形,求最大的矩形面积
这道题目可以用暴力的方法O(n^2),用单调栈的话就是O(n);
单调栈是递增的,这个单调栈在入栈的时候要合并矩形,合并之后再入栈。
暴力ac的代码
#include <iostream>
#include <string.h>
#include <math.h>
#include <stdlib.h>
#include <algorithm>
using namespace std;
#define MAX 50000
struct Node
{
int w;
int h;
}a[MAX+5];
int n;
int ans;
int sum;
int main()
{
while(scanf("%d",&n)!=EOF)
{
if(n==-1)
break;
ans=0;
sum=0;
for(int i=0;i<n;i++)
scanf("%d%d",&a[i].w,&a[i].h);
for(int i=0;i<n;i++)
{
sum=0;
for(int j=i+1;j<n;j++)
{
if(a[j].h>=a[i].h)
sum+=a[i].h*a[j].w;
else
break;
}
for(int p=i-1;p>=0;p--)
{
if(a[p].h>=a[i].h)
sum+=a[i].h*a[p].w;
else
break;
}
sum+=a[i].w*a[i].h;
ans=max(ans,sum);
}
printf("%d\n",ans);
}
return 0;
}
单调栈优化的代码
#include <iostream>
#include <string.h>
#include <stdlib.h>
#include <algorithm>
#include <math.h>
#include <stack>
using namespace std;
#define MAX 50000
int n;
struct Node
{
int x,y;
}a[MAX+5];
stack<Node> Stack;
int ans;
int main()
{
while(scanf("%d",&n)!=EOF)
{
if(n==-1)
break;
while(!Stack.empty())
Stack.pop();
ans=0;
for(int i=0;i<n;i++)
scanf("%d%d",&a[i].x,&a[i].y);
Stack.push(a[0]);
for(int i=1;i<n;i++)
{
int sum=0;
Node term=Stack.top();
while(term.y>a[i].y)
{
sum+=term.x;
ans=max(ans,sum*term.y);
Stack.pop();
if(Stack.empty())
break;
term=Stack.top();
}
Node temp;
temp.x=sum+a[i].x;
temp.y=a[i].y;
Stack.push(temp);
}
int sum=0;
while(!Stack.empty())
{
Node term=Stack.top();
sum+=term.x;
ans=max(ans,sum*term.y);
Stack.pop();
}
printf("%d\n",ans);
}
}
这里并不能看出单调栈的求区间最大值的功能,反而是运用了单调栈单调的特性,进行求解。这也是单调栈,单调队列这种数据结构有魅力的地方吧