单调数据结构的个人研究
常用于处理权值与序号单调的问题。
最近较小数
给定一个长度为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;
}
代码很短吧。
滑动窗口(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。
最大矩形面积 (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;
}
代码是分块写的,思路还算清晰。
音乐会的等待(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,多加了一个边界判断。
最大数(P1198)
现在请求你维护一个数列,要求提供以下两种操作:
1、 查询操作。
语法:Q L
功能:查询当前数列中末尾L个数中的最大的数,并输出这个数的值。
限制:L不超过当前数列的长度。(L > 0)(L>0)
2、 插入操作。
语法:A n
功能:将n加上t,其中t是最近一次查询操作的答案(如果还未执行过查询操作,则t=0),并将所得结果对一个固定的常数D取模,将所得答案插入到数列的末尾。
限制:n是整数(可能为负数)并且在长整范围内。
注意:初始时数列是空的,没有一个数。
第一行两个整数,M和D,其中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。
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);
}
}
把若干个循环写一起了,所以代码看上去很短,但细节不少。
P2698 [USACO12MAR]Flowerpot S
给出N滴水的坐标,y表示水滴的高度,x表示它下落到x轴的位置。
每滴水以每秒1个单位长度的速度下落。你需要把花盆放在x轴上的某个位置,使得从被花盆接着的第1滴水开始,到被花盆接着的最后1滴水结束,之间的时间差至少为D。
我们认为,只要水滴落到x轴上,与花盆的边沿对齐,就认为被接住。给出N滴水的坐标和D的大小,请算出最小的花盆的宽度W。
输入格式
第一行2个整数 N 和 D。
第2.. N+1行每行2个整数,表示水滴的坐标(x,y)。
输出格式
仅一行1个整数,表示最小的花盆的宽度。如果无法构造出足够宽的花盆,使得在D单位的时间接住满足要求的水滴,则输出-1。
输入输出样例
输入 #1
4 5
6 3
2 4
4 10
12 15
输出 #1
2
说明/提示
【样例解释】
有4滴水, (6,3), (2,4), (4,10), (12,15).水滴必须用至少5秒时间落入花盆。花盆的宽度为2是必须且足够的。把花盆放在x=4..6的位置,它可以接到1和3水滴, 之间的时间差为10-3 = 7满足条件。
【数据范围】
40%的数据:1 ≤ N ≤ 1000,1 ≤ D ≤ 2000;
100%的数据:1 ≤ N ≤ 100000,1 ≤ D ≤ 1000000,0≤x,y≤10^6。
先抽象一下题意,把水滴看成点,然后选择一个最小区域使得在该区域内最高点与最低点的y坐标差大于题中给定数字D。
类似前面那几题的思路,先用栈维护可以成为答案的序列,然后一个一个统计取最小就好了,解答写得太简单,建议先自己想一下再看,有一个让我觉得奇怪的地方,看样例,跨越4..6的花盆长度为2?
附上代码:
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 1e5 + 10;
struct Drop {
int x, y;
Drop(int x = 0, int y = 0): x(x), y(y) {}
bool operator < (const Drop& rhs) const {
return x < rhs.x || (x == rhs.x && y < rhs.y);
}
} drops[maxn];
int stack[maxn], top;
int n, D;
int lower(int );
int better(int, int);
int main() {
scanf("%d%d", &n, &D);
for(int i = 1, x, y; i <= n; i++) {
scanf("%d%d", &x, &y);
drops[i] = Drop(x, y);
}
sort(drops + 1, drops + 1 + n);
int ans = -1;
for(int i = 1; i <= n; i++) {
if(top && drops[i].y - D >= drops[stack[0]].y) {
int pos = lower(drops[i].y - D);
ans = better(ans, drops[i].x - drops[pos].x);
}
while(top && drops[stack[top-1]].y >= drops[i].y) top--;
stack[top++] = i;
}
printf("%d
", ans);
return 0;
}
int lower(int x) {
int l = 0, r = top - 1, mid, ans;
while(l <= r) {
mid = (l + r) >> 1;
if(drops[stack[mid]].y <= x) { l = mid + 1; ans = mid; }
else r = mid - 1;
}
return stack[ans];
}
int better(int x, int y) {
if(x == -1) return y;
if(x < y) return x;
else return y;
}
待更新中......