[洛谷P1886]滑动窗口 (单调队列)(线段树)
---恢复内容开始---
这是很好的一道题
题目描述:
现在有一堆数字共N个数字(N<=10^6),以及一个大小为k的窗口。
现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。
例如:
队列 [1 3 -1 -3 5 3 6 7]
窗口大小为3.
则如下图所示:
输入输出格式:
输入格式:
输入一共有两行,第一行为n,k。
第二行为n个数(<INT_MAX).
输出格式:
输出共两行,第一行为每次窗口滑动的最小值
输入样例:
8 3 1 3 -1 -3 5 3 6 7
输出样例:
-1 -3 -3 -3 3 3 3 3 5 5 6 7
解决方案:
(一)st表
(二)线段树
这里用到了两个结构体,然后就是进行普通的线段树求最大最小,这里就不再赘述了q
第一个结构体是查询用的
第二个结构体就是线段树了,这里我用了一个构造函数;
其实这些操作只是为了加速我们的线段树过程(让它别T)
不过总体地实现还是相对比较优美(复杂)的q
Code:
#include<iostream> #include<cstdio> #include<cmath> #include<algorithm> #define inf 2147483647 using namespace std; int a[12345678],n,k; struct search_tree { int minn; int maxn; }q; struct Segtree { int minv[12345678],maxv[12345678]; void pushup(int rt) { maxv[rt] = max(maxv[rt<<1],maxv[rt<<1|1]); minv[rt] = min(minv[rt<<1],minv[rt<<1|1]); } void build(int rt,int l,int r) { if(l == r) { maxv[rt] = a[l]; minv[rt] = a[l]; return ; } int mid = (l + r)>>1; build(rt<<1,l,mid); build(rt<<1|1,mid+1,r); pushup(rt); } search_tree solve(int rt,int l,int r,int ll,int rr) //ll rr 为待求量 { if(ll <= l && rr >= r) return (search_tree) { minv[rt], maxv[rt] }; int mid = (l+r)>>1; int minn = inf , maxn = -inf; search_tree ans; if(ll <= mid) { ans = solve(rt<<1,l,mid,ll,rr); maxn = max(maxn,ans.maxn); minn = min(minn,ans.minn); } if(rr > mid) { ans = solve(rt<<1|1,mid+1,r,ll,rr); maxn = max(maxn,ans.maxn); minn = min(minn,ans.minn); } return (search_tree) { minn, maxn }; } }segtree; int main() { scanf("%d%d",&n,&k); for(int i=1;i<=n;i++) scanf("%d",&a[i]); segtree.build(1,1,n); for(int i=1;i<=n - k + 1;i++) { q = segtree.solve(1,1,n,i,i + k - 1); printf("%d ",q.minn); a[i] = q.maxn; } printf("\n"); for(int i=1;i<=n-k+1;i++) printf("%d ",a[i]); return 0; }
(三)单调队列
单调队列概念:
-
队列中的元素其对应在原来的列表中的顺序必须是单调递增的。
-
队列中元素的大小必须是单调递*(增/减/甚至是自定义也可以)
这保证了单调队列的双有序
但是单调队列有一个特殊的特点就是可以双向操作出队。
但是我们并不是把单调队列里的每一个数都要存一遍,我们只需要存储这些单调队列里有序环境中有序的数(即我们所要求的目的)
这个概念还是很抽象的q
不过从这个题来看还是可以有所帮助的q
并不是每一个数的记录都是有意义的;
我们只需要存储那些对于我们来说有意义的数值;
以此题求最小值为栗子:
若有ai和aj两个数,且满足i<j。
如果ai>aj,那么两个数都应该记录;
但是如果ai≤aj,那么当aj进入区间后,ai的记录就没有意义了。
我们假设每个数能作为区间最大值的次数(即它可以存在区间内的次数)为它的贡献,当aj进入区间以后,在区间中存在的时间一定比ai长,也就说明ai一定不会再做贡献了;
我们确定没有贡献的点,便可以直接删去
Code:
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #define ll long long #define MAXN 1008666 using namespace std; struct Node { int v; int pos; }node[MAXN << 1]; int n,a[MAXN << 1],h = 1,t,k; int m; int main() { scanf("%d%d",&n,&k); for(int i=1;i<=n;i++) scanf("%d",&a[i]); for(int i=1;i<=n;i++) //维护单调递减队列 { while(h <= t && node[h].pos + k <= i) h++; while(h <= t && node[t].v >= a[i]) t--; node[++t].v = a[i]; node[t].pos = i; if(i >= k) printf("%d ",node[h].v); } h = 1; t = 0; printf("\n"); for(int i=1;i<=n;i++) //维护单调递增队列 { while(h <= t && node[h].pos + k <= i) h++; while(h <= t && node[t].v <= a[i]) t--; node[++t].v = a[i]; node[t].pos = i; if(i >= k) printf("%d ",node[h].v); } return 0; }