题解 P1886 滑动窗口 /【模板】单调队列

囧仙

2020-08-03 17:46:45

Solution

## 前言 - 尽管题目名称是单调队列模板,但这并不能阻止我们用其他的思路。 - 这篇题解简要介绍了一个**线性**的分块做法,它的复杂度不亚于单调队列。 - 同时,这篇文章分析了这种方法的优劣,希望你能学到一些。 ## 题解 滑动窗口的询问操作,有一个特殊的性质:**每个询问的长度相同**,都为 $k$ 。 这个性质告诉我们,任何询问都会恰好包含 $k,2\times k,\cdots ,\lfloor\frac{n}{k}\rfloor \times k$ 这 $\lfloor\frac{n}{k}\rfloor-1$ 个节点中的某个节点。 考虑题目要求的 $\min$ 和 $\max$ 操作。我们发现,它们满足结合律。因此,如果我们能够将一个询问拆成两个区间,使得它们的并为这个询问,那么我们就能通过合并它们来直接求得答案。 于是,我们将上述节点定义为**关键点**。枚举每个关键点,然后分别向左右拓展,计算以它为左/右端点的区间最大值和最小值,直到碰到另外一个关键点或边界。于是, 每个询问都能通过它包含的关键点的信息得出。 以题面的样例 $a=\{1,3,-1,-3,5,3,6,7\},k=3$ 为例,计算滑块的最小值: - 枚举到关键点 $3$ 。 - 分别计算出 $[3,3],[2,3],[1,3]$ 的最小值,以及 $[3,4],[3,5]$ 的最小值。 - 计算出 $[1,3],[2,4]=[2,3]\cup[3,4],[3,5]$ 的最小值分别为 $-1,-3,-3$ 。 - 枚举到关键点 $6$ 。 - 分别计算出 $[6,6],[5,6],[4,6]$ 的最小值,以及 $[6,7],[6,8]$ 的最小值。 - 计算出 $[4,6],[5,7]=[5,6]\cup [6,7],[6,8]$ 的最小值分别为 $-3,3,3$ 。 最大值过程同理。 该算法的复杂度为 $\mathcal O(n)$ ,因为我们从每个关键点向左右拓展的次数为 $4\times k$ ,而一共有 $\lfloor \frac{n}{k}\rfloor$ 个关键点。 ## 参考代码 ```cpp #include<bits/stdc++.h> #define up(l,r,i) for(int i=l;i<r;i++) using namespace std; typedef long long LL; const int INF =2147483647; int qread(){ int w=1,c,ret; while((c=getchar())> '9'||c< '0') w=(c=='-'?-1:1); ret=c-'0'; while((c=getchar())>='0'&&c<='9') ret=ret*10+c-'0'; return ret*w; } const int MAXN =2e6+3; int n,k,A[MAXN],B[MAXN],C[MAXN],P[MAXN],Q[MAXN]; int main(){ n=qread(),k=qread(); up(0,n,i) A[i]=qread(); for(int i=k-1;i<n;i+=k){ B[i]=C[i]=A[i]; int j=i+1; B[j]=C[j]=A[j]; up(1,k,t) B[i-t]=min(B[i-t+1],A[i-t]); up(1,k,t) C[i-t]=max(C[i-t+1],A[i-t]); up(1,k,t) B[j+t]=min(B[j+t-1],A[j+t]); up(1,k,t) C[j+t]=max(C[j+t-1],A[j+t]); up(j-k,j,t) P[t]=min(B[t],B[t+k-1]),Q[t]=max(C[t],C[t+k-1]); } up(0,n-k+1,i) printf("%d ",P[i]); puts(""); up(0,n-k+1,i) printf("%d ",Q[i]); puts(""); return 0; } ``` ## 分析 - 这种方法和单调队列复杂度相同,码量略少。 - 单调队列还可以处理滑块长度不一定固定的情况。该方法可能会有问题。 - 这种方法能够处理下方问题: > 有一个长度为 $n$ 的整数序列 $a$ 。现在要求求出区间 $[i,i+m-1]$ 的**连乘积**对 $p$ 取模的值。 单调队列难以维护;而求前缀积及其逆元的方法,无法处理逆元不存在的情况。 这种时候,我们就能用上述的分块做法,直接维护每个关键点向左右延伸的前缀/后缀积。