单调栈和单调队列是两种很简单,但是很强大的数据结构.一般不会直接出裸题,常常作为优化手段使用.(多见于dp)
单调队列实际上是升级版的单调栈.
单调栈可以在O(n)时间复杂度下完成以下操作
对于给定的a[ ]数组,在O(n)时间内生成数组 L[ ],其中L[i]表示a[i] 左边/右边 第一个 小于/大于 a[i]的元素的下标(常记录下标,可以使得操作更灵活).
对于朴素算法(O(n^2))来说,这已经是一个非常大的优化了.
其思想就是维护栈的单调性(当然维护方法不唯一,具体情况具体分析,灵活运用)\
以(自栈顶到栈底)递增栈为例
第一种是如果当前想要插入的元素的值大于栈顶元素值,则弹出栈顶,继续比较,直到栈顶元素小于(也可能是小于等于,具体情况具体分析),或是栈为空,则插入(当然,插入之前的栈顶值就是L[i]的值)
第二种是如果当前想要插入的元素的值大于栈顶元素值,则不插入 (仅限于很特殊的题目,和这里的问题貌似不太相符合,以下我们只讨论第一种维护形式)
我们拿一个题举例子
A histogram is a polygon composed of a sequence of rectangles aligned at a common base line. The rectangles have equal widths but may have different heights. For example, the figure on the left shows the histogram that consists of rectangles with the heights 2, 1, 4, 5, 1, 3, 3, measured in units where 1 is the width of the rectangles:
Usually, histograms are used to represent discrete distributions, e.g., the frequencies of characters in texts. Note that the order of the rectangles, i.e., their heights, is important. Calculate the area of the largest rectangle in a histogram that is aligned at the common base line, too. The figure on the right shows the largest aligned rectangle for the depicted histogram.
The input contains several test cases. Each test case describes a histogram and starts with an integer n, denoting the number of rectangles it is composed of. You may assume that 1 <= n <= 100000. Then follow n integers h1, …, hn, where 0 <= hi <= 1000000000. These numbers denote the heights of the rectangles of the histogram in left-to-right order. The width of each rectangle is 1. A zero follows the input for the last test case.
For each test case output on a single line the area of the largest rectangle in the specified histogram. Remember that this rectangle must be aligned at the common base line.
这个题实际上有dp的解法,也有单调栈的解法,两种方法的思路都是枚举每个小矩形作为最后最大的矩形的顶部高度,就是遍历整个图形,假设当前的矩形就是最后的矩形的最高点,然后算出最大面积,然后最后取所有答案中的最大值即可.
算法正确性显然.我们使用两个数组,L[ ],R[ ],L[i]表示如果第i个矩形是顶峰,则其构成的最大矩形的最左侧的矩形的下标,R[ ]表示其右侧的下标,最后矩形的面积就是
max i n − 1 [ ( R [ i ] − L [ i ] + 1 ) ∗ a [ i ] ] \max_i^{n-1}[(R[i]-L[i]+1)*a[i]] imaxn−1[(R[i]−L[i]+1)∗a[i]]
我们先讨论L[ ]的求解方法.L[i]的值实际上是第i个矩阵左侧第一个比它矮的矩阵的下标+1
我们就考虑用单调栈解决此问题
for(int i=0; i<n; ++i) {
while(!s.empty()&&a[s.top()]>=a[i]) {
s.pop();
}
if(s.empty()) {
l[i] = 0; // 其左边的元素都比它大,则左边界就是第一个矩阵
} else {
l[i] = s.top()+1; // 左侧第一个比它矮的矩阵的下标+1
}
s.push(i); // 记录下标比记录值要灵活
}
同理我们再对R[ ] 如法炮制,也可以在O(n)时间内解决
#include
#define ll long long
using namespace std;
int a[100000+100];
int l[100000+100];
int r[100000+100];
int main() {
int n;
while(cin>>n,n) {
stack<int> s;
stack<int> s2;
for(int i=0; i<n; ++i) {
cin>>a[i];
}
for(int i=0; i<n; ++i) {
while(!s.empty()&&a[s.top()]>=a[i]) {
s.pop();
}
if(s.empty()) {
l[i] = 0;
} else {
l[i] = s.top()+1;
}
s.push(i);
}
for(int i=n-1; i>=0; --i) {
while(!s2.empty()&&a[s2.top()]>=a[i]) {
s2.pop();
}
if(s2.empty()) {
r[i] = n-1;
} else {
r[i] = s2.top()-1; // 注意细节
}
s2.push(i);
}
ll maxn=0;
for(int i=0; i<n; ++i) {
maxn = max(maxn,(ll)(r[i]-l[i]+1)*(ll)a[i]);
}
cout<<maxn<<endl;
}
return 0;
}
实际上仅仅看单调队列的队尾的话,就是一个单调栈,而其队首可出队的特性,使他变成了单调栈的升级版.
单调队列常用于解决这样的问题(又名 滑动窗口)
给一个数列a[ ],和一个连续区间长度k,让输出所有a中每个 长度为K的连续区间中的最大值
这个题也是采用和单调栈相同的思路,首先要维护区间长度,如果后进的元素比队尾大,则一直弹栈直到栈顶(队尾)元素小于要插入的元素,再将元素入队.这个正确性可以简单证明:
这是一种贪心思想,我们假设队尾的元素的下标是目前窗口的尾部,则此元素前面的,且比它值小的元素没有存在的意义,因为只要他只要是整个队中最大的元素,在他未出队前,此区间中的最大值一定是他.
An array of size n ≤ 1e6 is given to you. There is a sliding window of size k which is moving from the very left of the array to the very right. You can only see the k numbers in the window. Each time the sliding window moves rightwards by one position. Following is an example: The array is [1 3 -1 -3 5 3 6 7], and k is 3.
Window position | Minimum value | Maximum value |
---|---|---|
[1 3 -1] -3 5 3 6 7 | -1 | 3 |
1 [3 -1 -3] 5 3 6 7 | -3 | 3 |
1 3 [-1 -3 5] 3 6 7 | -3 | 5 |
1 3 -1 [-3 5 3] 6 7 | -3 | 5 |
1 3 -1 -3 [5 3 6] 7 | 3 | 6 |
1 3 -1 -3 5 [3 6 7] | 3 | 7 |
Your task is to determine the maximum and minimum values in the sliding window at each position.
The input consists of two lines. The first line contains two integers n and k which are the lengths of the array and the sliding window. There are n integers in the second line.
There are two lines in the output. The first line gives the minimum values in the window at each position, from left to right, respectively. The second line gives the maximum values.
8 3
1 3 -1 -3 5 3 6 7
-1 -3 -3 -3 3 3
3 3 5 5 6 7
#include
#include
#define ll long long
using namespace std;
int a[1000000+100];
int main() {
int n,m;
scanf("%d%d",&n,&m);
for(int i=0; i<n; ++i) {
scanf("%d",&a[i]);
}
deque<int> minn,maxn;
for(int i=0; i<m; ++i) {
if(minn.empty()) {
minn.push_back(i);
} else {
while(!minn.empty()&&a[minn.back()]>=a[i]) {
minn.pop_back();
}
minn.push_back(i);
}
if(maxn.empty()) {
maxn.push_back(i);
} else {
while(!maxn.empty()&&a[maxn.back()]<=a[i]) {
maxn.pop_back();
}
maxn.push_back(i);
}
}
bool sp=0;
for(int i=0; i<=n-m; ++i) {
if(sp) {
printf(" ");
} else {
sp=1;
}
printf("%d",a[minn.front()]);
if(i==minn.front()) {
minn.pop_front();
}
if(i+m<n) {
while(!minn.empty()&&a[minn.back()]>=a[i+m]) {
minn.pop_back();
}
minn.push_back(i+m);
}
}
printf("\n");
sp=0;
for(int i=0; i<=n-m; ++i) {
if(sp) {
printf(" ");
} else {
sp=1;
}
printf("%d",a[maxn.front()]);
if(i==maxn.front()) {
maxn.pop_front();
}
if(i+m<n) {
while(!maxn.empty()&&a[maxn.back()]<=a[i+m]) {
maxn.pop_back();
}
maxn.push_back(i+m);
}
}
printf("\n");
return 0;
}
二维单调队列顾名思义,就是在二维的角度使用单调队列维护区间性质.
bzoj1047 理想的正方形
有一个
a*b
的整数组成的矩阵,现请你从中找出一个n*n
的正方形区域,使得该区域所有数中的最大值和最小值的差最小。
经由之前的讨论,不难发现,对于一维的情况并不难解决.O(n)之内扫一遍就可以了,然后可以得到一个数组c[]
,c[i](i>=n)
表示在[i-n,i]
范围内,区间的最大值与最小值之差.
(给一个1维的数组,求长度为n的区间内最大值和最小值的差的最小值)
对于二维的情况,我们尝试向一维转化.我们先按照一维的情况,对每一行去计算出其对应的maxn[],minn[]
.然后我们发现,对于一个n*n
矩阵,我们只要再对n行maxn[],minn[]
求优先队列即可.
换句话说,就是第一步将列
这个属性给压缩了,maxn[]
中一个元素就相当于n列了,所以我们再对每n行maxn[]
进行操作就能找到n*n
区间内的最大值了,最小值也能同理求出.
#include
using namespace std;
#define ll long long
#define MAXN 1100
int mp[MAXN][MAXN];
int maxn[MAXN][MAXN];
int minn[MAXN][MAXN];
int minrec[MAXN][MAXN];
int maxrec[MAXN][MAXN];
int main() {
int a, b, n;
scanf("%d%d%d", &a, &b, &n);
for (int i = 1; i <= a; ++i) {
for (int j = 1; j <= b; ++j) {
scanf("%d", &mp[i][j]);
maxn[i][j] = 0;
minn[i][j] = 0;
minrec[i][j] = 0;
maxrec[i][j] = 0;
}
}
// 处理maxn[]
for (int i = 1; i <= a; ++i) {
deque<int> q;
for (int j = 1; j <= b; ++j) {
while (!q.empty() && mp[i][q.back()] <= mp[i][j]) {
q.pop_back();
}
q.push_back(j);
if (q.front() <= j - n) {
q.pop_front();
}
maxn[i][j] = mp[i][q.front()];
}
}
// 处理minn[]
for (int i = 1; i <= a; ++i) {
deque<int> q;
for (int j = 1; j <= b; ++j) {
while (!q.empty() && mp[i][q.back()] >= mp[i][j]) {
q.pop_back();
}
q.push_back(j);
if (q.front() <= j - n) {
q.pop_front();
}
minn[i][j] = mp[i][q.front()]; // j is end of line
}
}
// 对maxn[]求单调队列,生成maxrec[]
for (int j = n; j <= b; ++j) {
deque<int> q;
for (int i = 1; i <= a; ++i) {
while (!q.empty() && maxn[q.back()][j] <= maxn[i][j]) {
q.pop_back();
}
q.push_back(i);
if (q.front() <= i - n) {
q.pop_front();
}
maxrec[i][j] = maxn[q.front()][j];
}
}
// 对minn[]求单调队列,生成minrec[]
for (int j = n; j <= b; ++j) {
deque<int> q;
for (int i = 1; i <= a; ++i) {
while (!q.empty() && minn[q.back()][j] >= minn[i][j]) {
q.pop_back();
}
q.push_back(i);
if (q.front() <= i - n) {
q.pop_front();
}
minrec[i][j] = minn[q.front()][j];
}
}
// 计算最后的结果,注意合法的范围是(n,n)及其右下角的空间内
int ans = maxrec[n][n] - minrec[n][n];
for (int i = n; i <= a; ++i) {
for (int j = n; j <= b; ++j) {
ans = min(ans, maxrec[i][j] - minrec[i][j]);
}
}
printf("%d\n", ans);
return 0;
}
Planting Trees
给出n*n的矩阵,要求选出尽可能大的子矩阵,使得子矩阵中的高度差不超过m.
1<=n<=500 0<=m<=1e5 1<=aij<=1e5
当然…题目中还特意指出多组输入下,n^3<=2.5e8,就是在暗示这个题的复杂度是O(n^3)
既然是n3算法,我们就考虑枚举一下子区间.一个子矩阵有四条边,我们不难发现,最多枚举三条边(n3).而且枚举之后应该是O(1)算出结果.这个难度是比较大的.所以我们考虑枚举上下边界,考虑O(n)时间内求出左右区间以使得满足题意.
根据做单调队列的经验,我们发现我们只能在O(n)时间内处理一个一维数组.所以我们就考虑将上下边界范围内的数组给压缩到一维(仅保留最大值和最小值就行,这个操作是O(n)的,因为可以分摊到枚举下边界的情况下).然后对这个压缩后的数组进行单调队列就行.这个单调队列的写法类似于尺取法,也就是尽可能地大.然后处理完这个数组之后,其实就得到了左右边界,然后我们再根据上下边界,就得到了子矩阵的大小.取最大值即可.
(顺带一提,这个题卡stl的队列,所以以下的版本是自己手写的队列(为了让main可读性更好一些))
#include
using namespace std;
#define ll long long
#define MAXN 1100
int mp[MAXN][MAXN];
int colmax[MAXN];
int colmin[MAXN];
struct a {
int l, r;
int num[MAXN];
bool empty() {
return l == r;
}
void push_back(int x) {
num[r++] = x;
}
void pop_back() {
--r;
}
void pop_front() {
++l;
}
int back() {
return num[r - 1];
}
int front() {
return num[l];
}
};
int main() {
int T;
scanf("%d", &T);
while (T--) {
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
scanf("%d", &mp[i][j]);
}
}
int ans = 0;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
colmax[j] = 0;
colmin[j] = 200000;
}
for (int j = i; j <= n; ++j) {
for (int k = 1; k <= n; ++k) {
colmax[k] = max(colmax[k], mp[j][k]);
colmin[k] = min(colmin[k], mp[j][k]);
}
a q1;
a q2;
q1.l = 0;
q1.r = 0;
q2.l = 0;
q2.r = 0;
int l = 0;
for (int k = 1; k <= n; ++k) {
// 尺取,移动右边界
while (!q1.empty() && colmax[q1.back()] <= colmax[k]) {
q1.pop_back();
}
q1.push_back(k);
// 其实这一步也是移动右边界,只是更新最小值即可
while (!q2.empty() && colmin[q2.back()] >= colmin[k]) {
q2.pop_back();
}
q2.push_back(k);
// 发现不符合要求的地方,于是移动左边界,使只符合条件(可以看出,区间内只有一个元素的时候一定符合题意,所以这个一定能调整到合法位置)
while (!q1.empty() && !q2.empty() && colmax[q1.front()] - colmin[q2.front()] > m) {
++l;
while (!q1.empty() && q1.front() <= l) {
q1.pop_front();
}
while (!q2.empty() && q2.front() <= l) {
q2.pop_front();
}
}
ans = max(ans, (k - l) * (j - i + 1));
}
}
}
printf("%d\n", ans);
}
return 0;
}