單調佇列,單調棧總結
最近幾天接觸了單調佇列,還接觸了單調棧,就總結一下。
其實單調佇列,和單調棧都是差不多的資料型別,顧名思義就是在棧和佇列上加上單調,單調遞增或者單調遞減。當要入棧或者入隊的時候,要和棧頭或者隊尾進行比較,滿足單調的性質則入隊入棧,否則將當前元素刪去,直到滿足單調性質。
那麼問題來了,單調佇列,和單調棧有什麼用了。最普遍的最重要的作用就是起到優化的作用。當然我目前也只知道這個所用。
先看一道例題:
求一個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揹包和完全揹包,直接用單調佇列就可以。多重揹包優化,要把根據餘數把體積分成幾類
參考這個資料的
//
// 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);
}
}
這裡並不能看出單調棧的求區間最大值的功能,反而是運用了單調棧單調的特性,進行求解。這也是單調棧,單調佇列這種資料結構有魅力的地方吧