1. 程式人生 > >單調佇列,單調棧總結

單調佇列,單調棧總結

最近幾天接觸了單調佇列,還接觸了單調棧,就總結一下。
其實單調佇列,和單調棧都是差不多的資料型別,顧名思義就是在棧和佇列上加上單調,單調遞增或者單調遞減。當要入棧或者入隊的時候,要和棧頭或者隊尾進行比較,滿足單調的性質則入隊入棧,否則將當前元素刪去,直到滿足單調性質。
那麼問題來了,單調佇列,和單調棧有什麼用了。最普遍的最重要的作用就是起到優化的作用。當然我目前也只知道這個所用。
先看一道例題:
求一個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 &#169; 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 &#169; 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 &#169; 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);
    }
}

這裡並不能看出單調棧的求區間最大值的功能,反而是運用了單調棧單調的特性,進行求解。這也是單調棧,單調佇列這種資料結構有魅力的地方吧