单调队列与单调栈 初步
虽然是个不算复杂的数据结构,但是总感觉自己好像不太会,所以做一些题目以期能更好地掌握该知识点。
对于每个元素而言,要找在其之后第一个大于它的元素,所以我们可以维护一个单调减栈;当新进入的元素进入后将之前某些小的元素挤出栈后,由于单调栈的性质可知这个元素就是大于之前那些等待配对的出栈元素的第一个元素,所以就可以记录出栈元素的答案。
以区间最小元素的求解为例。
维护一个单调增的队列,当进入元素小于队尾元素,将其下沉并挤出那些大于它的元素;与此同时我们还需要保证最小的元素在k区间内,所以在队头要将那些序号已经不在当前k区间的元素退队。
如果当前某个人之后进入了某个更高的人,那么此人就会被遮挡而无法有可能与之后的人配对,所以根据这个特性就可以维护一个单调减栈。当进入一个新人后,单调栈中那些高度不高于他的人均可以与其配对(并出栈),除此以外如果还有一个比他高的人也可以进行一次配对,不过再往高处就不能看见了。
虽然理清了解答办法但是我写WA了…而且不知道怎么WA了…
学习一下简洁漂亮的标答:
using par = pair<int, int>;
stack<par> sta;
int main(){
// Fast;
int n; scanf("%d", &n);
ll ans = 0;
for(int i = 0, x; i < n; i++){
scanf("%d", &x);
par temp = {x, 1};
while(!sta.empty() && sta.top().first <= x){
ans += sta.top().second;
if(sta.top().first == x) temp.second += sta.top().second;
sta.pop();
}
if(!sta.empty()) ans++;
sta.push(temp);
}
printf("%lld\n", ans);
return 0;
}
poj2559 Largest Rectangle in a Histogram
最大矩形覆盖。
在这个序列中,如果出现了某个逆序对,那么高矩形的高度就无法再往后传递了;据此我们就可以得知应该使用单调增的单调栈模型。
一旦某个较矮的矩形进入,那么就应该弹出栈顶元素并求该高度可能覆盖的的面积最大值。因为单调栈的单调性,所以某个弹出元素的序号到当前元素序号之间的所有矩形高度都是大于等于弹出高度的,所以可以得到右矩形的最远延伸距离。而左边最远的延伸距离呢?可以考虑一组数据(5; 2 1 2 1 2),如果当之后出现的某些元素挤出之前的那些较高元素后,其实左边的高矩形不是被真正地移除,而是被截断
,所以可以为每个高度赋一个权值(可以理解为长度),这个较矮高度的矩形进入单调栈后每挤掉一个原有矩形就不断累加它的长度。
这样子在更新某高度最大面积覆盖的时候就可以利用权值(长度)、出栈元素序号和高度以及进栈元素的序号解得。
struct node{int id, num, x;};
stack<node> sta;
int main(){
// Fast;
int n;
while(~scanf("%d", &n) && n){
while(!sta.empty()) sta.pop();
ll ans = 0;
for(int i = 0, x; i <= n; i++){
if(i != n) scanf("%d", &x); else x = -1;
node temp = {i, 1, x};
while(!sta.empty() && x <= sta.top().x){
ans = max(ans, (ll)(i - 1 - sta.top().id + sta.top().num) * sta.top().x);
temp.num += sta.top().num;
sta.pop();
}
sta.push(temp);
}
printf("%lld\n", ans);
}
return 0;
}
poj2796 Feel Good
找一段区间使得其区间元素和乘上区间最小值的结果最大。
如果之后某个元素的值较小,那么之前那些更大的最小值显然就无法贡献到之后的区间了,因而我们选择单调减的单调栈。
与上题类似地考虑那些出栈元素对答案的更新问题。记录前缀和,用出栈元素前一个元素的位置和当前进栈元素的位置即可获得这个出栈元素所能贡献到的最长区间,乘上其本身的值即可获得这一段区间的幸福值。不断更新答案即可。
const int maxn = 1e5 + 10;
struct node{int id, x;};
stack<node> sta;
ll s[maxn];
int main(){
// Fast;
int n; scanf("%d", &n);
ll ans = 0, ansl = 0, ansy = 0;
for(int i = 0, x; i <= n; i++){
if(i != n) scanf("%d", &x); else x = -1;
s[i] = i == 0? x: s[i - 1] + x;
node temp = {i, x};
while(!sta.empty() && sta.top().x >= x){
int m = sta.top().x; sta.pop();
ll left = !sta.empty()? s[sta.top().id]: 0;
if((s[i - 1] - left) * m > ans){
ans = max(ans, (s[i - 1] - left) * m);
ansl = !sta.empty()? sta.top().id + 1: 0;
ansy = i - 1;
}
}
sta.push(temp);
}
printf("%lld\n%lld %lld\n", ans, ansl+ 1, ansy + 1);
return 0;
}
(ansy打错,实为ansl)
有一个小WA点,ansl和ansr初始化不应该选为-1,因为如果输入序列为全0时,max不会执行更新操作,所以左右区间也就不会被更新;置为0即可解决问题。
poj3250 Bad Hair Day
找每个元素之后的那些比他小的元素,一旦遇到不小于它的元素即停止寻找;求每个元素之和。
最开始读错题意了,以为只有遇到那些严格大于它的元素才会停止搜索。
用一个单调减的单调栈即可:当之后某个元素较高时会挡住之前较矮的元素,所以就应该挤掉那些小元素。
const int maxn = 8e4 + 10;
struct node{int id, h, num;};
stack<node> sta;
int ans[maxn];
int main(){
// Fast;
int n; scanf("%d", &n);
for(int i = 0, x; i < n; i++){
scanf("%d", &x);
node temp = {i, x, 1};
while(!sta.empty() && x >= sta.top().h){
ans[sta.top().id] = i - 1 - sta.top().id;
sta.pop();
}
sta.push(temp);
}
while(!sta.empty()){
ans[sta.top().id] = n - 1 - sta.top().id ;
sta.pop();
}
ll sum = 0;
for(int i = 0; i < n; i++) sum += ans[i];
printf("%lld\n", sum);
return 0;
}
今天没做几个题,日后有机会做一下更加困难的单调栈单调队列相关的题目。
总而言之,单调栈的核心思想在于贪心
, 只记录那些以后还可能有贡献的元素,而将对之后情况绝对无贡献的元素去除掉。