单调队列单调栈
单调队列
问题引入
RMQ(x, y)为询问数组[x, y]区间内部最小值,如: RMQ(0, 3) = 1
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
3 | 1 | 4 | 5 | 2 | 9 | 8 | 12 |
固定尾部查询区间最小值: RMQ(x, 7), 最少记录几个元素,可以满足RMQ(x, 7)要求
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
1 | 2 | 8 | 12 |
此时即构成了一个单调队列
维护固定结尾的区间最值问题
入队操作:
入队前,把之前破坏单调性元素从队尾移出(维护单调性)
出队操作:
如果队首元素超出区间范围,就将元素从对手出队
滑动窗口(#271)
原理:分别维护增减单调队列
a[N]数组,q[] head = tail = 0 存储单调队列下标,a[q[head]]为极值
首先将k-1个元素进入队列,先判断是否单调,tail-head && a[q[tail - 1]] >= a[i]; q[tail++] = i;
当滑动到k元素时开始弹出极值,依然先维护单调性再进队,判断队长度大于k时踢出队首
#include<iostream>
using namespace std;
#define MAX_N 300000
int a[MAX_N + 5];
int q[MAX_N + 5], head = 0, tail = 0;//队列存储数组下标
int main() {
int n, k;
cin >> n >> k;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i < k; i++) {
while (tail - head && a[q[tail - 1]] >= a[i]) tail--;//意图在队尾进一个元素,首先判断是否符合单调性,不符合元素踢出
q[tail++] = i;//进队
}
for (int i = k; i <= n; i++) {
while (tail - head && a[q[tail - 1]] >= a[i]) tail--;
q[tail++] = i;
if (q[head] <= i - k) head++;//判断队长
i == k || cout << " ";
cout << a[q[head]];
}
cout << endl;
head = tail = 0;
for (int i = 1; i < k; i++) {
while (tail - head && a[q[tail - 1]] <= a[i]) tail--;
q[tail++] = i;
}
for (int i = k; i <= n; i++) {
while (tail - head && a[q[tail - 1]] <= a[i]) tail--;
if (q[head] <= i - k) head++;
q[tail++] = i;
i == k || cout << " ";
cout << a[q[head]];
}
cout << endl;
return 0;
}
优化:
for (int i = 1; i <= n; i++) {
while (tail - head && a[q[tail - 1]] >= a[i]) tail--;
q[tail++] = i;
if (i < k) continue;
if (q[head] <= i - k) head++;
i == k || cout << " ";
cout << a[q[head]];
}
最大子序列和(#270)
前缀和&&差分
s0 | s1=s0+a1 | s2=s1+a2 | s3=s2+a3 | … | sn=sn-1+an |
---|---|---|---|---|---|
a0 | a1 | a2 | a3 | … | an |
x0 | x1=a1-a0 | x2=a2-a1 | x3=a3-a2 | … | xn=an-an-1 |
原理:前缀和序列运算可以得到区间和
维护单调递增的前缀和序列,并维护窗口大小
固定结尾,向前找序列最小值,相减就是序列和最大值
a[] = 1 -3 5 1 -2 3; s[] = 1 -2 3 4 2 5
#include<iostream>
using namespace std;
#define MAX_N 300000
int s[MAX_N + 5];
int q[MAX_N + 5], head, tail;
int main() {
int n, m, ans;
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> s[i], s[i] += s[i - 1];
ans = s[1];
for (int i = 1; i <= n; i++) {
if (q[head] < i - m) head++;
ans = max(ans, s[i] - s[q[head]]);
while (tail - head && s[q[tail -1]] > s[i]) tail--;
q[tail++] = i;
}
cout << ans;
return 0;
}
关于判断队长时是否取等号
滑动窗口要取,因为下一次循环必有元素进入;
最长子序列不用,下次未必有元素进入队列,窗口长度可以小于m
总结
单调队列就两步:
- 维护队列单调性,新元素进入队列(入队前踢出当前所有违反单调性元素)
- 维护窗口长度,超过则弹出队首元素(队首元素,区间最值)
单调栈
问题引入
给一个序列,求序列中,每个元素左侧,第一个小于它的元素
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|
3 | 1 | 4 | 5 | 2 | 9 | 8 | 12 |
相较于单调队列,单调栈没有维护窗口长度操作,单调队列没窗口的话自动取前方最值
维护最近大于/小于问题
从左侧先入栈,就是维护左侧最近关系;从右侧先入栈,就是维护右侧最近关系。
最大矩形面积(#264)
原理:选定基准板,往两边查找第一个小于基准元素,相减即为矩形边长
就是一半的单调队列问题,组成单调栈后,获取最近的小于本元素值位置
编程技巧:加入哨兵元素,保证扁矩形也符合规则
#include<iostream>
using namespace std;
#define MAX_N 100000
long long a[MAX_N + 5];
long long s[MAX_N + 5], top = -1;
long long l[MAX_N + 5], r[MAX_N + 5];
int main() {
long long n;
cin >> n;
for (long long i = 1; i <= n; i++) cin >> a[i];
a[0] = a[n + 1] = -1;
s[top = 0] = 0;//清空栈
for (long long i = 1; i <= n; i++) {
while(a[s[top]] >= a[i]) top--;
l[i] = s[top];
s[++top] = i;
}
s[top = 0] = n + 1;
for (long long i = n; i >= 1; i--) {
while (a[s[top]] >= a[i]) top--;
r[i] = s[top];
s[++top] = i;
}
long long ans = 0;
for (long long i = 1; i <= n; i++) {
ans = max(ans, a[i] * (r[i] - l[i] - 1));
}
cout << ans << endl;
return 0;
}
双生序列(#372)
#include<iostream>
using namespace std;
#define MAX_N 500000
int a[MAX_N + 5];
int b[MAX_N + 5];
int sa[MAX_N + 5], sa_top = -1;
int sb[MAX_N + 5], sb_top = -1;
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) cin >> b[i];
sa[sa_top = 0] = 0, sb[sb_top = 0] = 0;
int num = 1;
for (int i = 1; i <= n; i++){
while (a[sa[sa_top]] > a[i]) sa_top--;
sa[++sa_top] = i;
while (b[sb[sb_top]] > b[i]) sb_top--;
sb[++sb_top] = i;
if (sa_top != sb_top) {
num = i - 1;
break;
}
}
cout << num << endl;
return 0;
}
优化:
#include<iostream>
using namespace std;
#define MAX_N 500000
int a[MAX_N + 5], b[MAX_N + 5];
//检查过程会踢出元素,可以利用检查过的空间存储单调栈元素
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i++) cin >> a[i];
for (int i = 0; i < n; i++) cin >> b[i];
int p = 1, topa = -1, topb = -1;
while (p < n) {
while (topa != -1 && a[p] <= a[topa]) topa--;
while (topb != -1 && b[p] <= b[topb]) topb--;
if (topa - topb) break;
a[++topa] = a[p];
b[++topb] = b[p];
p++;
}
cout << p << endl;
return 0;
}
压栈和进队列不同点
栈top指针指向栈顶元素;队列tail指针指向队尾元素+1