单调队列优化dp

六眼飞鱼酱① 提交于 2019-11-30 18:19:26

使用单调队列优化DP,那么必会有求i之前某个范围的极值的操作,这类DP的方程通常为:

F[i]=min(F[j]+a[i]:j<i)

(a[i]是与j无关的数。


定义:队列元素保持单调递增(减),而保持的方式就是通过插队,把队尾破坏了单调性的数全部挤掉,从而使队列元素保持单调 。

那么单调队列有什么用呢?优化DP。许多单调队列优化的DP可以使复杂度直接降维,下面就以最简单的一道题为例:

Description

  烽火台又称烽燧,是重要的军事防御设施,一般建在险要或交通要道上。一旦有敌情发生,白天燃烧柴草,通过浓烟表达信息;夜晚燃烧干柴,以火光传递军情(晓荣的历史课讲过吼),在某两座城市之间有 n 个烽火台,每个烽火台发出信号都有一定代价。为了使情报准确地传递,在连续 m 个烽火台中至少要有一个发出信号。请计算总共最少花费多少代价,才能使敌军来袭之时,情报能在这两座城市之间准确传递。

Input

  第一行:两个整数 N,M。其中N表示烽火台的个数, M 表示在连续 m 个烽火台中至少要有一个发出信号。接下来 N 行,每行一个数 Wi,表示第i个烽火台发出信号所需代价。

Output

  一行,表示答案。

Sample Input

5 3
1
2
5
6
2

Sample Output

4

Data Constraint

对于50%的数据,M≤N≤1,000 。 对于100%的数据,M≤N≤100,000,Wi≤100。


分析题目,由于题目要求连续m个烽火台中至少要有一个发出信号,很容易得出DP转移方程:

F[i]=min(F[j]:i−m<j<i)+a[i]F[i]=min(F[j]:i−m<j<i)+a[i]

最直接的方法是枚举状态,对于每一个i,我们在i-m+1到i-1中寻找一个最小的F[j]进行状态转移,枚举状态的时间复杂度是O(n),寻找最小值的状态时间复杂度是O(n),因此这种方法的复杂度是O(n^2)。题目的是数据范围是n<=100000,显然超时
那么怎么用单调队列优化呢?
---------------------

e.g.状态枚举到i,当m=4时,我们要做的就是在i-3到i-1中找到最小的F[j],那么枚举到i+1时,我们要做的就是要在i-2到i中找到最小的F[j]。

要寻找最小值的区间向后移动了一位,也就是F[i-m+1]的值被抛弃,F[i-1]的值被加入。
这里就可以用单调队列处理了,F[i-1]是插队的数据,F[i-1]有资格插队是因为它更优且更靠近i (又年轻又比你优秀),比它更差的数将被它取代,保留那些数据没有任何好处。而那些已经不再维护区间之外的就不必再对其进行维护,出队即可。看了代码会更加明白:

#include<bits/stdc++.h> 
#define rep(i,a,b) for(int i=a;i<=b;i++)
using namespace std;

int read()
{
    int x=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
    return x*f;
}
int n,m,ans=2147483647,head=1,tail=0;
int q[100010],a[100010],f[100010];

int main()
{
    n=read(),m=read();
    
    rep(i,1,n)a[i]=read();
    
    rep(i,1,n)
    {
        while(head<=tail && f[i-1]<=f[q[tail]])tail--;
        //注意<=要取等(虽然我们一样优,
        但是我比你年轻啊!) 
        q[++tail]=i-1; //当F[i-1]比队尾值更优时把队
        尾值弹出,并插入 
        
        while(head<=tail && q[head]<i-m)head++;//不属于区间维护内的数弹出
        f[i]=f[q[head]]+a[i];
    }
    
    for(int i=n;i>n-m;i--)
        ans=min(ans,f[i]);

    printf("%d",ans);
    return 0;
}

大菜来了

JZOJ 1772 假期
Description

经过几个月辛勤的工作,FJ决定让奶牛放假。假期可以在1…N天内任意选择一段(需要连续),每一天都有一个享受指数W。但是奶牛的要求非常苛刻,假期不能短于P天,否则奶牛不能得到足够的休息;假期也不能超过Q天,否则奶牛会玩的腻烦。FJ想知道奶牛们能获得的最大享受指数。

Input

第一行:N,P,Q.
第二行:N个数字,中间用一个空格隔开,每个数都在longint范围内。

Output

一个整数,奶牛们能获得的最大享受指数。

Sample Input

5 2 4
-9 -4 -3 8 -6

Sample Output

5

Data Constraint

50% 1≤N≤10000
100% 1≤N≤100000
1<=p<=q<=n
Hint
选择第3-4天,享受指数为-3+8=5。

怎么样?有木有很熟悉?对,mzoj 1354: 最大子序列的和原题本题(只是换了一个题目背景)


思路:

看到区间的问题首先肯定是想到求前缀和

我们把[1,k]的
和记为sum[k],可以得到sum[i] = sum[i - 1] + a[i],
[l,r]的和即为sum[r] - sum[l - 1](这里视sum[0] =
0)。(减法原理)

我们假设选择的区间为[l,r]且r固定,可知
r−B+1≤l≤r−A+1若要使[l,r]区间的值最大,则sum[l - 1]
需最小,才可使得sum[r] - sum[l - 1]最小。

当i右移一位到i+1,因为p,q为给定不变的值,对应寻
找最小sum[l-1]的区间也右移一位
#include<bits/stdc++.h> 
#define ll long long 
#define N 1000010
#define rep(i,a,b) for(int i=a;i<=b;i++)
using namespace std;

int read()
{
    int x=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
    return x*f;
}

ll sum[N],q[N];
ll n,ans=-2147483647,A,B;
 
int main()
{
    //freopen("input.txt","r",stdin);
    n=read(),A=read(),B=read();
    
    rep(i,1,n)
        sum[i]=sum[i-1]+read();
    
    int head=0,tail=1;
    
    rep(i,A,n)
    {
        while(head<=tail && q[head]<i-B)//不处于维护范围内的,出队
            head++;
            
        while(head<=tail && sum[i-A]<=sum[q[tail]])//更优的sum[l - 1]予以插队
            tail--;
            
        q[++tail]=i-A;
        ans=max(ans,sum[i]-sum[q[head]]);//更新答案
    }
    printf("%lld\n",ans);
    return 0;
}

# 顶级大菜!理想的正方形

用单调队列分别维护行与列。

先用单调队列对每一行的值维护,并将a[][]每个区间的最大值,最小值分别存在X[][]和x[][]中。
那么X[][]与x[][]所存储的分别是1×n的长方形内的最大值,最小值。X[i][j]存储第i行第j~j+n-1列的长方形中的
最大值。同理,x[i][j]存储第i行第j~j+n-1列的长方形中的最小值。
再对这两个数组的每一列上的值进行维护,将X[][]中每个区间的的最大值用Y[][]维护,将x[][]中的每个区间
的最小值用y[][]维护。那么Y[i][j]存储X[][]中第i~i+n-1行第j列的长方形的最大值。同理y[i][j]存储x[][]中
第i~i+n-1行第j列的长方形的最小值。

故Y[i][j]存储的实为以a[i~i+n-1][j~j+n-1]中的最大,即以i,j为左上角,边长为n的正方形中的最大值。同理,
y[i][j]存储的即以i,j为左上角,边长为n的正方形中的最小值。

#include <bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define N 1005

int n,m,k,front,FRONT,back,BACK,ans;
int a[N][N],q[N],Q[N];
int x[N][N],X[N][N];
int y[N][N],Y[N][N];

int read()
{
    int x=0,f=1;char c=getchar();
    while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
    while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
    return x*f;
}

int main()
{
    n=read(),m=read(),k=read();
    rep(i,1,n)
        rep(j,1,m)
            a[i][j]=read();

    rep(i,1,n)
        {
            FRONT=BACK=front=back=Q[1]=q[1]=1;
            rep(j,2,m)
                {
                    while (a[i][j]>=a[i][Q[BACK]]&&FRONT<=BACK) BACK--;
                    while (a[i][j]<=a[i][q[back]]&&front<=back) back--;
                    BACK++;back++;Q[BACK]=j;q[back]=j;
                    
                    while (j-Q[FRONT]>=k) FRONT++;
                    while (j-q[front]>=k) front++;
                    if (j>=k) X[i][j-k+1]=a[i][Q[FRONT]],x[i][j-k+1]=a[i][q[front]];
                }
        }
        
    rep(i,1,m-k+1)
    {
        FRONT=BACK=front=back=Q[1]=q[1]=1;
        rep(j,2,n)
            {
                while (X[j][i]>=X[Q[BACK]][i]&&FRONT<=BACK) BACK--;
                while (x[j][i]<=x[q[back]][i]&&front<=back) back--;
                BACK++;back++;Q[BACK]=j;q[back]=j;
                while (j-Q[FRONT]>=k) FRONT++;
                while (j-q[front]>=k) front++;
                if (j>=k) Y[j-k+1][i]=X[Q[FRONT]][i],y[j-k+1][i]=x[q[front]][i];
            }
    }
        
    ans=0x3f3f3f3f;
    
    rep(i,1,n-k+1)
        rep(j,2,m-k+1)
            ans=min(ans,Y[i][j]-y[i][j]);
        
    printf("%d\n",ans);
    return 0;
}
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!