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 }
  • 相关阅读:
    CSS 浮动
    函数defined
    SVN提交冲突
    抽象方法和抽象类
    Teco测试总结
    面向对象三大特性(封装,继承,多态)之多态
    PHP工厂模式的简单实现
    DOM 访问节点
    c程序的执行过程
    PHP实现菱形与杨辉三角形【php趣味案例】
  • 原文地址:https://www.cnblogs.com/zhenglw/p/10146373.html
Copyright © 2011-2022 走看看