【专题系列】单调队列优化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 }

 

posted @ 2018-12-19 21:53  zheng_liwen  阅读(944)  评论(0编辑  收藏  举报
/*去广告*/