单调数据结构与优先队列

Posted strong-will

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单调数据结构与优先队列相关的知识,希望对你有一定的参考价值。

单调栈,单调队列,优先队列

单调数据结构

常用于处理权值与序号单调的问题。

1.最近较小数

给定一个长度为n(n <= 5e6)的序列a(0 <= ai <= 1e9),求出每个数左边第一个较小数的下标,如果不存在输出0。

样例输入:

4   
4 3 5 7

样例输出:

0 0 2 3

这算是一道单调数据结构的入门题吧,考虑这样的情况:如果对于第i个数之前有一个下标为j的数,且a[i] <= a[j],那么对于第i个数之后的数字,j不可能是答案下标了(ai更小且i比j更靠近i之后的数字)。通过这个我们对序列正向扫描一遍,对答案下标维护一个单调递增的单调栈,先修改后查询,不懂的话可以对着代码造一组数据模拟一下。

code:

#include<cstdio>

const int maxn = 5e6 + 10;

int a[maxn], stack[maxn];
int n, top;

int main() {
    //读入数据 
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d", a + i);
    
    //单调栈维护答案序列,栈顶为实际顶端的上一个 
    for(int i = 1; i <= n; i++) {
        while(top && a[stack[top - 1]] >= a[i]) top--;
        
        if(top) printf("%d ", stack[top - 1]); 
        else printf("%d ", 0);
        stack[top++] = i;
    }
    return 0;
}

代码很短吧。

2.滑动窗口(P1886

有一个长为n的序列 a,以及一个大小为k的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值(题面参考原题)。

输入样例:

8 3
1 3 -1 -3 5 3 6 7

输出样例:

-1 -3 -3 -3 3 3
3 3 5 5 6 7

参考最近较小数的思路,先处理最小数,再处理较最大数,可以从头部弹出元素,我们维护两个指针front & rear,

先修改后查询。

Code:

#include<cstdio>

const int maxn = 1e6 + 10;

int a[maxn], deque[maxn], n, k, rear, front;

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(front != rear && a[deque[rear - 1]] >= a[i]) rear--;
        deque[rear++] = i;
        while(front != rear && deque[front] <= i - k) front++;
        
        if(i >= k) printf("%d ", a[deque[front]]);
    }
    printf("
");
    
    front = rear = 0;
    for(int i = 1; i <= n; i++) {
        while(front != rear && a[deque[rear - 1]] <= a[i]) rear--;
        deque[rear++] = i;
        while(front != rear && deque[front] <= i - k) front++;
        
        if(i >= k) printf("%d ", a[deque[front]]);
    }
    return 0;
}

代码好像没有什么地方可以强调的,deque使用数组模拟的,也可以使用STL里的模板deque。

3.最大矩形面积 (POJ2559)

地面上从左到右并排紧挨着摆放n(n <= 5e5)个矩形,已知这此矩形的底边宽度都为1,高度不完全相等。求在这些矩形包括的范围内能得到的面积最大的矩形,打印出该面积。所求矩形可以横跨多个矩形,但不能超出原有矩形所确定的范围(详细题面参考原题)。

样例输入:

7 
2 1 4 5 1 3 3
                                  _           
                               _ | |          
                              |H||H|    _  _  
                         _    |H||H|   | || | 
                        | | _ |H||H| _ | || | 
                        |_||_||H||H||_||_||_| (如图)

样例输出:

8

这题需要稍微动一动脑筋,并不只是一种解法,可以用单调数据结构做,做法如下:对于n个矩形,我们求出由它可以扩展出的最大矩形,即在这个最大矩形中它的高最小,我们找出每个矩形高对应的最小区间(最近较小数),然后用区间长度乘以当前矩形的高就是由这个矩形可以扩展出来的最大矩形,在这些矩形中取最大面积就是答案。

code:

#include<cstdio>

typedef long long LL;

const int maxn = 5e5 + 10;
int a[maxn], stack[maxn], Left[maxn], Right[maxn];
int top, n;

int main() {
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d", a + i);
    
    for(int i = 1; i <= n; i++) {
        while(top && a[stack[top - 1]] >= a[i]) top--;
        Left[i] = (top ? stack[top - 1] + 1: 1);
        stack[top++] = i;
    }
    
    top = 0;
    for(int i = n; i >= 1; i--) {
        while(top && a[stack[top - 1]] >= a[i]) top--;
        Right[i] = (top ? stack[top - 1] - 1 : n);
        stack[top++] = i;
    }
    
    LL ans = 0;
    for(int i = 1; i <= n; i++) 
        if(1ll * (Right[i] - Left[i] + 1) * a[i] > ans) 
            ans = 1ll * (Right[i] - Left[i] + 1) * a[i];

    printf("%lld
", ans);
    return 0;
}

代码是分块写的,思路还算清晰。

4.音乐会的等待(P1823)

题目描述

N个人正在排队进入一个音乐会。人们等得很无聊,于是他们开始转来转去,想在队伍里寻找自己的熟人。队列中任意两个人A和B,如果他们是相邻或他们之间没有人比A或B高,那么他们是可以互相看得见的。

写一个程序计算出有多少对人可以互相看见。

输入格式

输入的第一行包含一个整数N (1 ≤ N ≤ 500 000), 表示队伍中共有N个人。

接下来的N行中,每行包含一个整数,表示人的高度,以毫微米(等于10的-9次方米)为单位,每个人的调度都小于2^31毫微米。这些高度分别表示队伍中人的身高。

输出格式

输出仅有一行,包含一个数S,表示队伍中共有S对人可以互相看见。

输入输出样例

输入

7 
2 4 1 2 2 5 1

输出

10

如果知道单调栈的思路看到这道题是不是很简单?

防止一对互相看到关系多次被统计我们从左向右对于每个人只统计他/她向左可以看见的人。

维护一个可以看见的人的栈,然后二分查找,上界复杂度 o(nlogn)

Code:

#include<cstdio>

const int maxn = 5e5 + 10;
typedef long long LL;

int a[maxn], stack[maxn];
int n, top;

int upper(int);

int main() {
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) scanf("%d", a + i);
    
    LL ans = 0;
    for(int i = 1; i <= n; i++) {
        int pos = upper(a[i]);
        ans = ans + top - pos;
        while(top && a[stack[top - 1]] < a[i]) top--;
        stack[top++] = i;
    }
    
    printf("%lld
", ans);
    return 0;
}

int upper(int x) {
    if(!top || a[stack[0]] <= x) return 0; 
    
    int l = 0, r = top - 1, mid, ans;
    while(l <= r) {
        mid = (((r - l) >> 1) + l);
        if(a[stack[mid]] > x) { ans = mid; l = mid + 1; }
        if(a[stack[mid]] == x) r = mid - 1;
        if(a[stack[mid]] < x) r = mid - 1;
    }
    return ans;
}

手写的upper_bound,多加了一个边界判断。

5.最大数(P1198)

题目描述

现在请求你维护一个数列,要求提供以下两种操作:

1、 查询操作。

语法:Q L

功能:查询当前数列中末尾L个数中的最大的数,并输出这个数的值。

限制:L不超过当前数列的长度。(L > 0)(L>0)

2、 插入操作。

语法:A n

功能:将n加上t,其中t是最近一次查询操作的答案(如果还未执行过查询操作,则t=0),并将所得结果对一个固定的常数D取模,将所得答案插入到数列的末尾。

限制:n是整数(可能为负数)并且在长整范围内。

注意:初始时数列是空的,没有一个数。

输入格式

第一行两个整数,MD,其中M表示操作的个数(M≤200,000),D如上文中所述,满足(0<D<2,000,000,000)

接下来的M行,每行一个字符串,描述一个具体的操作。语法如上文所述。

输出格式

对于每一个查询操作,你应该按照顺序依次输出结果,每个结果占一行。

输入输出样例

输入 #1

5 100
A 96
Q 1
A 97
Q 1
Q 2

输出 #1

96
93
96

这题比较简单,可以用线段树做,也可以用单调数据结构做,细节自己想一想吧。

Code:

#include<cstdio>

const int maxn = 2e5 + 10;
typedef long long LL;

int a[maxn], stack[maxn];
int m, p, top, cnt, t;

int lower(int );

int main() {
    scanf("%d%d", &m, &p);
    
    LL x; char s[5];
    for(int i = 1; i <= m; i++) {
        scanf("%s%lld", s, &x);
        if(s[0] == 'A') {
            a[++cnt] = (x % p + t) % p;
            while(top && a[stack[top - 1]] <= a[cnt]) top--;
            stack[top++] = cnt;
        } else {
            t = a[stack[lower(cnt - x + 1)]];
            printf("%d
", t);
        }
    }
    return 0;
}

int lower(int x) {
    int l = 0, r = top - 1, mid, ans;
    while(l <= r) {
        mid = (((r - l) >> 1) + l);
        if(stack[mid] > x) {ans = mid; r = mid - 1; }
        if(stack[mid] == x) return mid;
        if(stack[mid] < x) l = mid + 1;
    } 
    return ans;
}

手写lower_bound,也可以直接用STL。

6.Blocks(P3503)

给出 N 个正整数 a[1.. N],再给出一个正整数 k,现在可以进行 如下操作:

每次选择一个大于 k 的正整数 a[i],将 a[i] 减去 1,选择 a[i - 1] 或 a[i + 1] 中的一个加上 1。

经过一定次数的操作后,问最大能够选出多长的一个连续子序列,使得这个子序列的每个数都不小于 k

总共给出 M 次询问,每次询问给出的 k 不同,你需要分别回答最长连续子序列的长度。

(N <= 1, 000, 000) (M <= 50) (k <= 1e9)

输入样例:

5 6
1 2 1 1 5
1 2 3 4 5 6

输出样例:

5 5 2 1 1 0

有一点繁琐,稍微错一点就会WA。

首先,所求的一段数的平均数大于k,我们让a中的每个数减去k后求前缀和,对前缀和维护一个单调递减的单调栈,然后从后向前扫一遍前缀和,弹栈,统计答案。

Code:

#include<cstdio>

const int maxn = 1e6 + 10;
typedef long long LL;

LL sum[maxn];
int a[maxn], stack[maxn], n, m, k, top;

int main() {
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++) scanf("%d", a + i);
    
    while(m--) {
        scanf("%d", &k);
        top = 1; stack[0] = 0;
        for(int i = 1; i <= n; i++) {
            sum[i] = sum[i - 1] + a[i] - k;
            
            if(sum[stack[top - 1]] > sum[i]) stack[top++] = i;
        }
        
        int ans = 0;
        for(int R = n; R >= 1 && top; R--) {
            if(sum[R] < sum[stack[top - 1]]) continue;
            while(top && sum[R] >= sum[stack[top - 1]]) top--;
            if(R - stack[top] > ans) ans = R - stack[top];
        }
        printf("%d ", ans);
    }
}

把若干个循环写一起了,所以代码看上去很短,但细节不少。

以上是关于单调数据结构与优先队列的主要内容,如果未能解决你的问题,请参考以下文章

数据结构——单调栈&单调队列(解决滑动窗口问题)

HDU 6957 Maximal submatrix(悬线法 || 优先队列 || 单调栈 )

优先队列和单调队列一样吗?

数据结构-优先队列与栈

单调队列单调栈优先队列模板

luogu 2827 蚯蚓 单调队列/优先队列