使用单调队列优化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; }