借助单调性处理问题,及时排除不可能的策略,以保持策略集合的有效性、秩序性。
单调栈
例题:POJ2559 直方图中的最大矩形(在一条直线上有N个宽度为1的矩形,求包含于这些矩形的并集内部的最大的矩形的面积)。
要求时间复杂度:O(N)
解题思路:
若矩形高度从左向右单调递增,则以每个矩形的高度乘以其到右边界的宽度得到一个面积,取这些面积中的最大值。
但若在下一个位置加入一个高度低于“右边界”的矩形,那么该矩形构成的最大面积矩形的高度不会大于其自身的高度,即该矩形前面比该矩形高的矩形高度对之后的矩形的最优解无影响(最优解不在其中),因此排除这些矩形,仅(使用栈)维护一个高度始终单调递增的矩形序列。
题解代码如下
// POJ2559
# include <cstdio>
# include <algorithm>
using namespace std;
# define ll long long
const int MAXN = 1e5;
struct { int h, x;} s[MAXN+1]; // h记录矩形高度,x记录矩形横坐标
int top; ll ans;
int main() {
int N; s[0].h = s[0].x = 0;
while (scanf("%d", &N), N) { // 时间复杂度:O(N)
top = ans = 0; // 清空栈,初始化
for (int i = 1; i <= N; i ++) {
int h; scanf("%d", &h);
if (h >= s[top].h && h) s[++top].h = h, s[top].x = i; // 直接入栈
else {
while (h < s[top].h) { // 取出栈顶直至栈为空,或栈顶矩形高度比当前矩形小
ans = max(ans, (ll)(i-1-s[top-1].x)*s[top].h); top --;
}
s[++top].h = h, s[top].x = i; // 入栈
}
}
// 把栈中剩余的矩阵弹出,更新答案
while (top) ans = max(ans, (ll)(N-s[top-1].x)*s[top].h), top --;
printf("%lld\n", ans);
}
return 0;
}
为方便理解,附朴素算法如下
// main
while (scanf("%d", &N), N) { // 时间复杂度:O(N^2)
long long ans = 0;
for (int i = 1; i <= N; i ++) scanf("%d", &h[i]);
// 朴素算法:统计以每个矩形高度为高所能构成的最大矩形面积
for (int i = 1; i <= N; i ++) {
int l1 = 0, l2 = 0;
for (int j = i-1; j; j --) if (h[j] >= h[i]) l1 ++; else break;
for (int j = i; j <= N; j ++) if (h[j] >= h[i]) l2 ++; else break;
ans = max(ans, (long long)h[i]*(l1+l2));
}
printf("%lld\n", ans);
}
单调队列
例题1:最大子序和(输入一个长度为 n 的整数序列,从中找出一段长度不超过 m 的连续子序列,使得子序列中所有数的和最大)。
要求时间复杂度:O(n)
解题思路:
求解最大子序列和,相当于求解
m
a
x
i
−
m
⩽
j
⩽
i
−
1
(
s
[
i
]
−
s
[
j
]
)
,
1
⩽
i
⩽
N
\underset{i-m\leqslant j\leqslant i-1}{max} (s[i]-s[j]) ,1\leqslant i\leqslant N
i−m⩽j⩽i−1max(s[i]−s[j]),1⩽i⩽N(s为前缀和序列)。
若固定i,则该问题等价于求解
m
i
n
i
−
m
⩽
j
⩽
i
−
1
s
[
j
]
\underset{i-m\leqslant j\leqslant i-1}{min} s[j]
i−m⩽j⩽i−1mins[j]。此时若出现s[j1]>s[j2](j1<j2),则对j2之后的i来说,s[j1]必不会为最优解(因为s[j2]更小且距离s[i]更近)。
因此枚举每一个i时,仅需维护一个单调递增的队列(双端队列,头尾均可进出的队列),队列头部的点即为该i点对应的最优解(保证头部距离i的长度不超过m的前提下)。
题解代码如下
# include <bits/stdc++.h>
using namespace std;
const int INF = ~(1<<31);
const int MAXN = 3e5;
int s[MAXN+1], head, tail;
struct { int val, p;} q[MAXN+1]; // val记录该点的数值,p记录该点的位置
int main() {
int n, m, a, ans = -INF; scanf("%d %d", &n, &m);
s[0] = 0; head = 1, tail = 0; // 初始化队列为空
for (int i = 1; i <= n; i ++) { // 枚举i,并将s[i-1]入队
scanf("%d", &a); s[i] = s[i-1] + a;
if (q[head].p < i-m) head ++; // 队列头部超出长度范围
while (s[i-1] <= q[tail].val && head <= tail) tail --; // 维护单调递增的队列
q[++tail].val = s[i-1], q[tail].p = i-1; // 入队
ans = max(ans, s[i] - q[head].val);
}
printf("%d\n", ans);
return 0;
}
例题2:POJ2823 Sliding Window(给定有n个数的序列,求滑动窗口长度为k时,每个滑动窗口中的最小值和最大值。输出第一行为n-k+1个最小值,第二行为n-k+1个最大值)。
要求时间复杂度:O(n)
解题思路:
类似例题1,对于最小值使用一个单调递增队列维护,最大值使用一个单调递减队列维护。现使用C++STL自带的双端队列(# include <deque>)实现。
方法 | 描述 |
---|---|
deque<TYPE> q | 定义双端队列q |
TYPE x = q[?] | 随机访问([0]为队首) |
TYPE x = q.front()/q.back() | 获得队首/尾元素 |
q.push_front(x)/q.push_back(x) | 从队首/尾入队 |
q.pop_front()/q.pop_back() | 从队首/尾出队 |
q.empty() | 队列为空则返回true |
题解代码如下
// POJ2823
# include <cstdio>
# include <algorithm>
# include <deque>
using namespace std;
const int INF = ~(1<<31);
const int MAXN = 1e6;
int mn[MAXN+1], mx[MAXN+1]; // 分别记录每个滑动窗口的最小值、最大值
struct Q { int val, p;};
deque<Q> qa, qd; // 递增双端队列qa,递减双端队列qd
int main() {
int n, k, x; scanf("%d %d", &n, &k); int h = min(n, k); Q t; // h用于处理k>n的情况
for (int i = 1; i <= n; i ++) {
scanf("%d", &x); t.val = x, t.p = i;
// 判断队列头部是否超出滑动窗口范围
if (!qa.empty() && i-qa.front().p >= k) qa.pop_front();
if (!qd.empty() && i-qd.front().p >= k) qd.pop_front();
// 维护队列单调性
while (!qa.empty() && x <= qa.back().val) qa.pop_back(); qa.push_back(t);
while (!qd.empty() && x >= qd.back().val) qd.pop_back(); qd.push_back(t);
// 记录当前窗口中的最小值、最大值
if (i >= h) mn[i-h+1] = qa.front().val, mx[i-h+1] = qd.front().val;
}
for (int i = 1; i <= n-h+1; i ++) printf("%d ", mn[i]); printf("\n");
for (int i = 1; i <= n-h+1; i ++) printf("%d ", mx[i]); printf("\n");
return 0;
}