文章目录
说明:
这类题用队列是存下标,不要直接存元素值,因为存下标:既能表示值,也能算间距
单调栈
例1.左边第一个比它小的数
题目:给定一个长度为N的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出-1
暴力做法
:
for (int i = 0; i < n; i++) {
int j = -1;
for (j = i - 1; j >= 0; j --) {
if (a[j] >= a[i]) continue;
cout << a[j] << endl;
break;
}
if (j == -1) cout << -1 << endl;
}
单调栈优化
- 可以发现如果当前元素是x,它的左边元素是a,且a > x,那么这个a在后面的元素求值时一定是毫无用处的,因为有个x比它小
- 利用上面的思想我们维护一个单调递增的队列(
存下标:既能表示值,也能算间距
)
C++代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 7;
int q[N],nums[N];
int hh, tt;
int main() {
int n;
cin >> n;
for (int i = 0; i < n; i ++) cin >> nums[i];
// 维护单调递增队列(存下标)
for (int i = 0; i < n; i ++) {
while(hh < tt && nums[i] <= nums[q[tt - 1]]) tt --;
if (hh < tt) cout << nums[q[tt - 1]] << " ";
else cout << -1 << " ";
q[tt ++] = i;
}
puts("");
return 0;
}
例2 直方图的最大矩形
算法分析:
维护一个单调递增的队列
/**
* @Author: Wilson79
* @Datetime: 2020年1月10日 星期五 20:55:01
* @Filename: Acwing 131.直方图中最大的矩形(单调栈).cpp
*/
#include <iostream>
using namespace std;
const int N = 1e5 + 7;
typedef long long LL;
int q[N], nums[N], Left[N], Right[N]; // (Left, Right数组存下标)
int n, hh = 0, tt = 0;
int main() {
while (cin >> n) {
if (n == 0) break;
for (int i = 0; i < n; i ++) {
cin >> nums[i];
}
// 单调栈
// 找到左边第一个比当前位置小的元素的下标,并存到Left数组中
hh = 0, tt = 0; // 多组测试数据
for (int i = 0; i < n; i ++) {
while(hh < tt && nums[q[tt - 1]] >= nums[i]) tt --;
if (hh < tt) Left[i] = q[tt - 1];
else Left[i] = -1;
q[tt ++] = i;
}
// 同理处理右半部分
hh = 0, tt = 0;
for (int i = n - 1; i >= 0; i --) {
while(hh < tt && nums[q[tt - 1]] >= nums[i]) tt --;
if (hh < tt) Right[i] = q[tt - 1];
else Right[i] = n;
q[tt ++] = i;
}
LL mx = 0;
for (int i = 0; i < n; i ++) {
mx = max(mx, nums[i] * 1ll * (1 + i - Left[i] - 1 + Right[i] - i - 1)); // 统计每个位置最大值
}
cout << mx << endl;
}
return 0;
}
例3.LeetCode42.接雨水(线性扫描/单调栈)
法一:
核心:每个矩形条上方所能接受的水的高度是由它左边最高的矩形和右边最高的矩形所决定的
/**
* @Author: Wilson79
* @Datetime: 2019年12月6日 上午09:44:29
* @Filename: 42.接雨水.cpp
*/
/* ====算法分析====
模拟 时间复杂度:O(n), 空间复杂度:O(n) 都是线性扫描
核心:每个矩形条上方所能接受的水的高度是由它左边最高的矩形和右边最高的矩形(包含当前位置)所决定的
先预处理出每个矩形左右两边的最大矩形高度,然后在遍历一遍数组即可
*/
class Solution {
public:
int trap(vector<int> &height) {
int n = height.size(), res = 0;
if (n == 0) return 0;
int left[n], right[n];
left[0] = height[0]; // 定义第一个的左边最大高度也是height[0]
for (int i = 1; i < n; i ++)
left[i] = max(left[i - 1], height[i]);
right[n - 1] = height[n - 1];
for (int j = n - 2; j >= 0; j --)
right[j] = max(right[j + 1], height[j]);
for (int i = 0; i < n; i++) {
res += min(left[i], right[i]) - height[i];
}
return res;
}
};
法二:
维护一个单调递减的栈
代码1
class Solution {
public:
static const int N = 1e4;
int q[N]; // 存下标比较好,既能访问值,也能算间距
int hh = 0, tt = 0;
int trap(vector<int>& nums) {
int n = nums.size();
int res = 0;
for (int i = 0; i < n;i ++) {
while(hh < tt && nums[q[tt - 1]] < nums[i]) {
int bottom = q[tt - 1];
tt --;
if (hh < tt) res += (i - q[tt - 1] - 1) * (min(nums[q[tt - 1]], nums[i]) - nums[bottom]);
}
q[tt ++] = i; // 包含了队空或nums[q[tt - 1]] >= nums[i]的情况
}
return res;
}
};
代码2
/* ====算法分析====
栈 时间复杂度:O(n), 空间复杂度:O(n)
维护一个单调递减的栈,while循环不断计算U型坑的雨水量
*/
class Solution {
public:
int trap(vector<int> &height) {
int n = height.size(), res = 0;
if (n == 0) return 0;
stack <int> stk; // 存下标
for (int i = 0; i < n; i ++) {
while(!stk.empty() && height[stk.top()] < height[i]) {
int bottom = stk.top();
stk.pop(); // pop后栈顶为U的左侧边界
if (!stk.empty()) {
res += (i - stk.top() - 1) * (min(height[stk.top()], height[i]) - height[bottom]);
}
}
stk.push(i); // 栈为空或值递减则入栈
}
return res;
}
};
滑动窗口
背景
n = 8,k = 3
每次输出滑动窗口的最小值
分析
-
滑动窗口:单调队列优化的最典型问题
-
思路:先想一想暴力如何做,然后从中
挖掘一些没有用的元素,删掉
,然后可以得到单调性,有单调性再去求极值,就可以直接拿第一个点或最后一个点,把本来要枚举一遍的时间复杂度变成O(1)了 -
用一个
队列
来维护,队列存放的是元素的下标
(用下标的好处时可以快速判断距离是否合法) -
暴力做法 O(nk),因为每次要遍历一次窗口
-
优化:通过观察可以发现,求最小值时,在靠左的且值较大的元素在后面一定是没有用的。所以每次加入一个元素时,维护这个队列,让它保持单调递增
如:5比3大,且靠左,那么只要3在时,5在后续的窗口中一定没有什么用,所以5直接出队列(那些早进入且比较大的数字就“永无出头之日”)
一般地
求最小值是维护单调递增队列
求最大值是维护单调递减队列
总结:先用栈或者队列暴力模拟整个题目,然后看一下栈或队列中哪些元素时没有用的,再把这些元素都删掉, 然后一般取队头或对尾
以剑指offer-滑动窗口的最大值为例
附:LeetCode239原题
算法分析:
- 维护一个双向单调队列,
队列存放的是元素的下标
(用下标的好处时可以快速判断距离是否合法)。 - 假设该双端队列的队头是整个队列的最大元素所在下标,至队尾下标代表的元素值依次降低。
- 一开始单调队列为空。随着对数组的遍历过程中,每次插入元素前,首先需要看队头是否还能留在队列中,如果队头下标距离i超过了k,则应该出队。
- 同时需要维护队列的单调性,如果nums[i]大于或等于队尾元素下标所对应的值,则当前队尾再也不可能充当某个滑动窗口的最大值了,故需要队尾出队。
- 始终保持队中元素从队头到队尾单调递减。依次遍历一遍数组,每次队头就是每个滑动窗口的最大值所在下标
C++代码
class Solution {
public:
static const int N = 1e6;
int q[N]; // 用队列存下标
int hh = 0, tt = 0;
vector<int> maxInWindows(vector<int>& nums, int k) {
int n = nums.size();
vector<int> res;
// tt指向待加入的位置,q[tt - 1]表示队尾元素,q[hh]表示队头元素
for (int i = 0 ; i < n; i ++) {
while (hh < tt && i - q[hh] >= k) hh ++; // 判断是否需要删除队头
while(hh < tt && nums[q[tt - 1]] < nums[i]) tt --;
q[tt ++] = i; // 包含了nums[q[tt - 1]] >- nums[i]和队列为空的情况
if (i >= k - 1) res.push_back(nums[q[hh]]);
}
return res;
}
};
滑动窗口求最大值和最小值
#include <iostream>
using namespace std;
const int N = 1e6 + 7;
int hh, tt;
int q[N], nums[N];
int main() {
int n, k;
cin >> n >> k;
for (int i = 0; i < n; i ++) {
cin >> nums[i];
}
for (int i = 0; i < n; i ++) {
while (hh < tt && i - q[hh] >= k) hh ++;
while (hh < tt && nums[q[tt - 1]] >= nums[i]) tt --;
q[tt ++] = i;
if (i >= k - 1) cout << nums[q[hh]] << " ";
}
cout << endl;
hh = 0, tt = 0;
for (int i = 0; i < n; i ++) {
while (hh < tt && i - q[hh] >= k) hh ++;
while (hh < tt && nums[q[tt - 1]] <= nums[i]) tt --;
q[tt ++] = i;
if (i >= k - 1) cout << nums[q[hh]] << " ";
}
cout << endl;
return 0;
}
424.替换后的最长重复字符
类似题目
LeetCode3.最长不重复字符串
LeetCode1004.最长连续的1
分析:也算滑动窗口,如果个数超过k了,就把“队头”的元素踢掉,如果没有超过“队尾”就可以继续添加元素
// 2020年1月12日 星期日 23:07:26
// 类似题目
// LeetCode3.最长不重复字符串
// LeetCode1004.最长连续的1
class Solution {
public:
int characterReplacement(string s, int k) {
int res = 0;
for (int t = 0; t < 26; t ++) {
int cnt = 0;
for (int i = 0, j = 0; i < s.size(); i ++) {
if (s[i] != 65 + t) cnt ++;
while(cnt > k) {
if (s[j] != 65 + t) cnt --;
j ++;
}
res = max(res, i - j + 1);
}
}
return res;
}
};