单调队列单调栈

单调队列单调栈

单调队列

问题引入

RMQ(x, y)为询问数组[x, y]区间内部最小值,如: RMQ(0, 3) = 1

01234567
314529812

固定尾部查询区间最小值: RMQ(x, 7), 最少记录几个元素,可以满足RMQ(x, 7)要求

01234567
12812

此时即构成了一个单调队列

维护固定结尾的区间最值问题

入队操作:

入队前,把之前破坏单调性元素从队尾移出(维护单调性)

出队操作:

如果队首元素超出区间范围,就将元素从对手出队


滑动窗口(#271)

link

原理:分别维护增减单调队列

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)

link

前缀和&&差分

s0s1=s0+a1s2=s1+a2s3=s2+a3sn=sn-1+an
a0a1a2a3an
x0x1=a1-a0x2=a2-a1x3=a3-a2xn=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

总结

单调队列就两步:

  • 维护队列单调性,新元素进入队列(入队前踢出当前所有违反单调性元素)
  • 维护窗口长度,超过则弹出队首元素(队首元素,区间最值)

单调栈

问题引入

给一个序列,求序列中,每个元素左侧,第一个小于它的元素

01234567
314529812

相较于单调队列,单调栈没有维护窗口长度操作,单调队列没窗口的话自动取前方最值

维护最近大于/小于问题

从左侧先入栈,就是维护左侧最近关系;从右侧先入栈,就是维护右侧最近关系。


最大矩形面积(#264)

link

原理:选定基准板,往两边查找第一个小于基准元素,相减即为矩形边长

就是一半的单调队列问题,组成单调栈后,获取最近的小于本元素值位置

编程技巧:加入哨兵元素,保证扁矩形也符合规则

#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)

link

#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

在Python中,单调栈单调队列是两种不同的数据结构单调栈是一个栈,它的特点是栈内的元素是单调的,可以是递增或递减的。在构建单调栈时,元素的插入和弹出都是在栈的一端进行的。与此类似,单调队列也是一个队列,它的特点是队列内的元素是单调的,可以是递增或递减的。在构建单调队列时,元素的插入是在队列的一端进行的,而弹出则是选择队列头进行的。 单调队列在解决某些问题时,能够提升效率。例如,滑动窗口最大值问题可以通过使用单调队列来解决。单调队列的结构可以通过以下代码来实现: ```python class MQueue: def __init__(self): self.queue = [] def push(self, value): while self.queue and self.queue[-1 < value: self.queue.pop(-1) self.queue.append(value) def pop(self): if self.queue: return self.queue.pop(0) ``` 上述代码定义了一个名为MQueue的类,它包含一个列表作为队列的存储结构。该类有两个方法,push和pop。push方法用于向队列中插入元素,它会删除队列尾部小于插入元素的所有元素,并将插入元素添加到队列尾部。pop方法用于弹出队列的头部元素。 总结来说,单调栈单调队列都是为了解决特定问题而设计的数据结构单调栈在构建时元素的插入和弹出都是在栈的一端进行的,而单调队列则是在队列的一端进行的。在Python中,可以通过自定义类来实现单调队列的功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值