【专题系列】单调队列优化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),而且常数也是十分优秀的
当然像这种求区间最值的问题也有一种简单粗暴的方法:线段树
代码如下:(这里代码就折叠掉了,有需求的读者可以自己阅读,就是求区间的最大值和最小值,连修改都不用,可以说是线段树的模板了)
View Code// 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; }
【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 }
对于本题,已经可以在要求的时间内求出答案了,但是显然这样的办法有点异常暴力,而且还要照顾一下不会线段树的童鞋是吧,于是切入正题,如何用单调队列做这题!
对于每一次状态的转移,我们只需要维护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】
【Hdu3530】Subsequence
【问题描述】
给定一个包含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 }