zoukankan      html  css  js  c++  java
  • 【专题系列】单调队列优化DP

    Tip:还有很多更有深度的题目,这里不再给出,只给了几道基本的题目(本来想继续更的,但是现在做的题目不是这一块内容,以后有空可能会继续补上)

    单调队列——看起来就是很高级的玩意儿,显然是个队列,而且其中的元素还具有单调性

    当然,它不只只是一个简单的队列,还是一个双端队列,即队首队尾都可以弹出元素,当然可以用C++自带的STL<deque>实现,当然这篇博客里不建议使用这种写法,因为不开O2的话就会有一个大弊端——

    单调队列裸题:滑动窗口

      线性的求一个区间内的最值,我们先来找个规律,比如这组数据:

      8 3
      1 3 -1 -3 5 3 6 7

     (以求最大值为例子)

      我们发现前两个数中 1  3   ,3的优先级明显大于1,原因?3比1大,而且3还在1的右边(这样在后面的更新中3还能起到作用,而1显然已经没有作用了,那么我们就可以把1从队列里面弹出去了!)当然,这个操作在3丢到队列里面的时候就可以进行了,同时在这个操作之前,我们还要把前面的元素和现在位置差距大于k的元素弹掉

      这样我们便可以保证队列中的元素是单调递增的,那么我们每次在n中取出k个元素的时候,只要把当前队列中的第k个元素放到输出列表中就好了!

      当然最小值也是一样的,维护一个单调递减的序列,在这个过程中,每次输出其中最小数

    代码如下:(先求最小值,后求最大值)

     1 #include<cstdio>
     2 #include<iostream>
     3 #include<cstring>
     4 #include<algorithm>
     5 using namespace std;
     6 inline int read(){
     7     int ans=0,f=1; char chr=getchar();
     8     while(!isdigit(chr)){if(chr=='-') f=-1;chr=getchar();}
     9     while(isdigit(chr)) {ans=(ans<<3)+chr-48;chr=getchar();}
    10     return ans*f;
    11 }
    12 void write(int x){
    13     if(x<0) x=-x,putchar('-');
    14     if(x>9) write(x/10);
    15     putchar(x%10+48);
    16 }
    17 int q[1000005],h,t,n,a[1000005],k;
    18 int main(){
    19     n=read();k=read();
    20     for(register int i=1;i<=n;++i) a[i]=read();
    21     h=1,t=0;
    22     for(register int i=1;i<=n;++i){//Min
    23         while(h<=t&&q[h]+k<=i) ++h;
    24         while(h<=t&&a[i]<=a[q[t]]) --t;
    25         q[++t]=i;
    26         if(i>=k) write(a[q[h]]),putchar(' ');
    27     }puts("");
    28     h=1,t=0;
    29     for(register int i=1;i<=n;++i){//Max
    30         while(h<=t&&q[h]+k<=i) ++h;
    31         while(h<=t&&a[i]>=a[q[t]]) --t;
    32         q[++t]=i;
    33         if(i>=k) write(a[q[h]]),putchar(' ');
    34     }
    35     return 0;
    36 }

    【时间复杂度分析】

       显然外循环的复杂度为n,关键在于其中的while循环,分析一下可以知道,每一个元素在其中只会进入队列一次,出队列一次,所以总的时间复杂度为O(n),而且常数也是十分优秀的

         当然像这种求区间最值的问题也有一种简单粗暴的方法:线段树

    代码如下:(这里代码就折叠掉了,有需求的读者可以自己阅读,就是求区间的最大值和最小值,连修改都不用,可以说是线段树的模板了)

    // luogu-judger-enable-o2
    #include<iostream>
    #include<cstdio>
    #include<cctype>
    #include<algorithm>
    #include<cstring>
    #define ll long long
    #define lson i << 1,l,m
    #define rson i << 1| 1,m + 1,r
    #define MAXN (int)1e6 + 5
    using namespace std;
    
    inline ll read(){
        char chr=getchar();
        ll f=1,ans=0;
        while(!isdigit(chr)) {if(chr=='-') f=-1;chr=getchar();}
        while(isdigit(chr))  {ans=ans*10;ans+=chr-'0';chr=getchar();}
        return ans*f;
    
    }
    
    void write(ll x){
        if(x<0){
            putchar('-');
            x=-x;
        }
        if(x<9)
            putchar(x+'0');
        else
            write(x/10),putchar(x%10+48);
    }
    
    struct P{
        ll l,r;
        ll max,add,min;
        ll mid(){
            return l + r >> 1;
        }
    }t[MAXN << 2];
    ll a[MAXN << 2];
    
    void build(ll i,ll l,ll r){
        t[i].l = l;t[i].r = r;
        if(l == r){
            t[i].min = a[l];
            t[i].max = a[l];
            return;
        }
        ll m = t[i].mid();
        build(lson);
        build(rson);
        t[i].max =max( t[i << 1].max , t[i << 1 | 1].max );
        t[i].min =min( t[i << 1].min , t[i << 1 | 1].min );
    }
    
    
    ll qmin(ll i,ll l,ll r){
        if(l <= t[i].l && t[i].r <= r) return t[i].min;
        ll pp = 0x3f3f3f3f,qq = 0x3f3f3f3f;
        ll m = t[i].mid();
        if(l <= m) pp = qmin(i << 1,l,r);
        if(r > m)  qq = qmin(i << 1 | 1,l,r);
        return min(qq , pp);
    }
    
    ll qmax(ll i,ll l,ll r){
        if(l <= t[i].l && t[i].r <= r) return t[i].max;
        ll pp = -0x3f3f3f3f,qq = -0x3f3f3f3f;
        ll m = t[i].mid();
        if(l <= m) pp = qmax(i << 1,l,r);
        if(r > m)  qq = qmax(i << 1 | 1,l,r);
        return max(qq , pp);
    }
    
    ll n,m;
    int main(){
        n = read() ;
        m = read() ;
        for(ll i = 1;i <= n;i ++)
            a[i] = read();
        build(1,1,n);
        for(int i = 1;i + m - 1 <= n;++i){
            printf("%lld",qmin(1,i,i+m-1));
            putchar(' ');
        }
        puts("");
        for(int i = 1;i + m - 1 <= n;++i){
            printf("%lld",qmax(1,i,i+m-1));
            putchar(' ');
        }
        return 0;
    }
    View Code

          

     【NO.1】

      【NOIP提高组初赛程序填空】烽火传递

    题目描述

    烽火台是重要的军事防御设施,一般建在交通要道或险要处。一旦有军情发生,则白天用浓烟,晚上有火光传递军情。

    在某两个城市之间有 nn 座烽火台,每个烽火台发出信号都有一定的代价。为了使情报准确传递,在连续 mm 个烽火台中至少要有一个发出信号。现在输入 n,mn,m 和每个烽火台的代价,请计算总共最少的代价在两城市之间来准确传递情报。

    输入格式

    第一行是 n,mn,m,表示 nn 个烽火台和连续烽火台数 mm;

    第二行 nn 个整数表示每个烽火台的代价 a_iai

    输出格式

    输出仅一个整数,表示最小代价。

    样例

    样例输入

    5 3
    1 2 5 6 2

    样例输出

    4

    样例说明

    在第 2,52,5 号烽火台上发信号。

    数据范围与提示

    n,m正整数且小于等于2×10^5

    【分析】

      显然是一道DP题

      不妨令f[i]表示取第i个点时的最小值

      那么有方程:

    f[i]=min{f[k]}+a[i]

    其中k∈[i-m,i-1]

      但显然这样是O(n^2)的复杂度,对于十万级的n,m显然不够优,如果我们可以在很短的时间内求出f[k](k∈[i-m,i-1])的最小值就好了,并且随着i的增大,它每次还能更新入新的数据!

      区间最小?单点更新?不就是线段树吗!O(nlogn)的算法出炉(还是一样,先把代码折叠起来,有需求的读者可以自己点开看):(当然,如果读者不会线段树,可以跳过这一部分的代码直接阅读后面)

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<cstring>
     4 #include<algorithm>
     5 #define int long long 
     6 using namespace std;
     7 inline int read(){
     8     char chr=getchar();    int f=1,ans=0;
     9     while(!isdigit(chr)) {if(chr=='-') f=-1;chr=getchar();}
    10     while(isdigit(chr))  {ans=(ans<<3)+(ans<<1);ans+=chr-'0';chr=getchar();}
    11     return ans*f;
    12 }
    13 void write(int x){
    14     if(x<0) putchar('-'),x=-x;
    15     if(x>9) write(x/10);
    16     putchar(x%10+'0');
    17 }
    18 int n,m,a[1000005<<1];
    19 int minn[1000005<<2];
    20 void updata(int i,int l,int r,int pos,int x){
    21     if(l==r){minn[i]=x;return;}
    22     int mid=l+r>>1;
    23     if(pos<=mid) updata(i<<1,l,mid,pos,x);
    24     else updata(i<<1|1,mid+1,r,pos,x);
    25     minn[i]=min(minn[i<<1],minn[i<<1|1]);
    26 }
    27 int query(int i,int l,int r,int ql,int qr){
    28     if(ql<=l&&r<=qr){return minn[i];}
    29     int mid=l+r>>1,x=0x3f3f3f3f,y=0x3f3f3f3f;
    30     if(ql<=mid) x=query(i<<1,l,mid,ql,qr) ;
    31     if(qr>mid) y=query(i<<1|1,mid+1,r,ql,qr);
    32     return min(x,y);
    33 }
    34 int f[1000005<<1];
    35 signed main(){
    36     freopen("ttt.in","r",stdin);
    37     n=read();m=read();
    38     for(int i=1;i<=n;i++)a[i]=read();
    39     for(int i=1;i<m;i++) updata(1,1,n,i,a[i]);
    40     for(int i=m;i<=n;i++){
    41         f[i]=query(1,1,n,i-m,i-1)+a[i];
    42         updata(1,1,n,i,f[i]);
    43     }int ans=0x7fffffff;
    44     for(int i=n-m+1;i<=n;i++) ans=min(f[i],ans);
    45     write(ans);
    46     return 0;
    47 } 
    View Code

      对于本题,已经可以在要求的时间内求出答案了,但是显然这样的办法有点异常暴力,而且还要照顾一下不会线段树的童鞋是吧,于是切入正题,如何用单调队列做这题!

      对于每一次状态的转移,我们只需要维护f[]数组的最值即可,那么显然我们可以以f[]创建一个单调队列,维护f[]的最小值,每次更新它的最新元素,且每次更新即取队首元素即可

      当然,上面的while循环可以换成在C++更加里面更加灵活的for循环,本质上还是一个求最值的问题,不过在这之前我们要能从中推出转移方程,关键是要从递推式中看出单调性,这也是我们用单调队列解题的前提

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<cstring>
     4 #include<algorithm>
     5 #define int long long 
     6 using namespace std;
     7 inline int read(){
     8     char chr=getchar();    int f=1,ans=0;
     9     while(!isdigit(chr)) {if(chr=='-') f=-1;chr=getchar();}
    10     while(isdigit(chr))  {ans=(ans<<3)+(ans<<1);ans+=chr-'0';chr=getchar();}
    11     return ans*f;
    12 }
    13 void write(int x){
    14     if(x<0) putchar('-'),x=-x;
    15     if(x>9) write(x/10);
    16     putchar(x%10+'0');
    17 }
    18 const int N=1e6+10;
    19 int n,m,l,r,a[N],f[N],q[N<<1];
    20 signed main(){
    21     n=read();m=read();
    22     for(int i=1;i<=n;i++) a[i]=read();
    23     l=r=0;
    24     for(int i=1;i<=n;i++){
    25         for(;l<r&&i-q[l]>m;l++);
    26         f[i]=f[q[l]]+a[i];
    27         for(;l<r&&f[q[r]]>f[i];r--);
    28         q[++r]=i;
    29     }int ans=0x7fffffff;
    30     for(int i=n-m+1;i<=n;i++) ans=min(ans,f[i]);//这里有一个小细节,最后一次选择可以从最后m个元素中选择,原因很简单,只要保证后面m个元素中有一个取就够了
    31     cout<<ans;
    32     return 0;
    33 }

     【NO.2】

      【Tyvj1305最大子序和

    【问题描述】

    输入一个长度为n的整数序列,从中找出一段不超过M的连续子序列,使得整个序列的和最大。
    例如 1,-3,5,1,-2,3
    当m=4时,S=5+1-2+3=7;
    当m=2或m=3时,S=5+1=6。
    

    【输入格式】

    第一行两个数n,m;
    第二行有n个数,要求在n个数找到最大子序和。
    

    【输出格式】

    一个数,数出他们的最大子序和。
    

    【输入样例】

    6 4
    1 -3 5 1 -2 3
    

    【输出样例】

    7
    

    【数据范围】

    n,m≤300000;数列元素的绝对值≤1000。
    

    【题目来源】

    Tyvj1305

    【问题分析】  

      首先,要求连续我们可以把原序列转化成前缀和进行求解

      因为前缀和的性质有sum[l~r]=sum[r]-sum[l-1],对于每一个r,我们只要求出前面m个数中最小的sum[l]即可保证sum[l~r]最大

      以sum[]建立单调队列即可

      

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<cstring>
     4 #include<algorithm>
     5 using namespace std;
     6 inline int read(){
     7     char chr=getchar();    int f=1,ans=0;
     8     while(!isdigit(chr)) {if(chr=='-') f=-1;chr=getchar();}
     9     while(isdigit(chr))  {ans=(ans<<3)+(ans<<1);ans+=chr-'0';chr=getchar();}
    10     return ans*f;
    11 }
    12 void write(int x){
    13     if(x<0) putchar('-'),x=-x;
    14     if(x>9) write(x/10);
    15     putchar(x%10+'0');
    16 }
    17 const int M=300005;
    18 int n,m;
    19 int q[M],a[M],h,t,f[M];
    20 int sum[M];
    21 int main(){
    22     n=read(),m=read();
    23     for(int i=1;i<=n;i++) a[i]=read(),sum[i]=sum[i-1]+a[i];
    24     int l=0,r=0;int ans=-0x7fffffff;
    25     for(int i=1;i<=n;i++){
    26         for(;l<r&&i-q[l]>m;l++);
    27         ans=max(ans,sum[i]-sum[q[l]]);
    28         for(;l<r&&sum[q[r]]>=sum[i];r--);
    29         q[++r]=i;
    30     }
    31     cout<<ans;
    32     return 0;
    33 }

     【NO.3】

      Hdu3530Subsequence

    【问题描述】

      给定一个包含n个整数序列,求满足条件的最长区间的长度:该区间内的最大数和最小数的差不小于m,且不大于k。

    【输入格式】

    输入包含多组测试数据:对于每组测试数据:
    第一行,包含三个整数n,m和k;
    第二行,包含n个整数的序列。

    【输出格式】

    对于每组测试数据,输出满足条件的最长区间的长度。

    【输入样例】

    5 0 0
    1 1 1 1 1
    5 0 3
    1 2 3 4 5

    【输出样例】

    5
    4

    【数据范围】

    1≤n≤100000;
    0≤m,k≤100000;
    0≤ai≤100000

    【题目来源】

    Hdu3530

    【题目分析】

      其实也是模板题了,只不过要求同时维护最大值最小值而已

      这里不再提最大值最小值的更新了,而是给出这道题里新的东西:要使最大值减最小值在区间[l,r]中的话,一旦当前的序列最大值减最小值不再该区间内了,便要继续弹出元素

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<cstring>
     4 #include<algorithm>
     5 using namespace std;
     6 inline int read(){
     7     char chr=getchar();    int f=1,ans=0;
     8     while(!isdigit(chr)) {if(chr=='-') f=-1;chr=getchar();}
     9     while(isdigit(chr))  {ans=(ans<<3)+(ans<<1);ans+=chr-'0';chr=getchar();}
    10     return ans*f;
    11 }
    12 void write(int x){
    13     if(x<0) putchar('-'),x=-x;
    14     if(x>9) write(x/10);
    15     putchar(x%10+'0');
    16 }
    17 const int M=100010;
    18 int q1[M],q2[M],a[M],n,m,k,t1,t2,tt1,tt2,ttt1,ttt2,ans;
    19 int main(){
    20     while(~scanf("%d%d%d",&n,&m,&k))
    21     {
    22         for(int i=1;i<=n;i++)    a[i]=read();
    23         memset(q1,0,sizeof(q1));
    24         memset(q2,0,sizeof(q2));
    25         t1=0;t2=0;ttt1=0;ttt2=0;ans=0;tt1=0;tt2=0;
    26         for(int i=1;i<=n;i++){
    27             while(t1<ttt1&&a[q1[ttt1-1]]<=a[i])ttt1--;  //maxn
    28             q1[ttt1++]=i;
    29             while(t2<ttt2&&a[q2[ttt2-1]]>=a[i])ttt2--;  //minn
    30             q2[ttt2++]=i;
    31             while(a[q1[t1]]-a[q2[t2]]>k)
    32                 if(q1[t1]<q2[t2]) tt1=q1[t1++];
    33                 else    tt2=q2[t2++]; 
    34             if(a[q1[t1]]-a[q2[t2]]>=m)
    35                 ans=max(ans,i-max(tt1,tt2));
    36         }
    37         write(ans),puts("");
    38     }
    39     return 0;
    40 }
  • 相关阅读:
    HDU 2844 Coins(多重背包)
    HDU 4540 威威猫系列故事——打地鼠(DP)
    Codeforces Round #236 (Div. 2)
    FZU 2140 Forever 0.5
    HDU 1171 Big Event in HDU(DP)
    HDU 1160 FatMouse's Speed(DP)
    ZOJ 3490 String Successor
    ZOJ 3609 Modular Inverse
    ZOJ 3603 Draw Something Cheat
    ZOJ 3705 Applications
  • 原文地址:https://www.cnblogs.com/zhenglw/p/10146373.html
Copyright © 2011-2022 走看看