单调栈

来源:互联网 发布:数据聚合是什么意思 编辑:程序博客网 时间:2024/06/05 14:20

放一道题目

#211. 乱头发节

题目描述

宁智贤的某 N 头奶牛 (1 <= N <= 80,000) 正在过乱头发节!由于每头牛都 意识到自己凌乱不堪的发型, 宁智贤希望统计出能够看到其他牛的头发的牛的数量。

每一头牛 i有一个高度 h[i] (1 <= h[i] <= 1,000,000,000)而且面向东方排成 一排(在我们的图中是向右)。因此,第i头牛可以看到她前面的那些牛的头, (即i+1, i+2,等等),只要那些牛的高度严格小于她的高度。

例如样例:

c[i] 表示第i头牛可以看到发型的牛的数量;请输出 c[1] 至 c[N]的和。 如上面的这个例子,正确解是3 + 0 + 1 + 0 + 1 + 0 = 5。

输入格式

Line 1: 牛的数量 N。

Lines 2..N+1: 第 i+1 是一个整数,表示第i头牛的高度。

输出格式

Line 1: 一个整数表示c[1] 至 c[N]的和。

样例数据

input

6

10

3

7

4

12

2

output

5

 

看这个图其实就很好理解了:

牛1可以看到2、3、4,到5的时候因为5比他高,所以5以后的牛他都看不到了。

2一头牛都看不到,因为3直接就比他高,所以3以后的牛他都看不到了。

3可以看到4,同样,5以后的都看不到。

4也是一头牛都看不到。

5可以看到6。

6是始终默认的0。

所以你就会发现这可以用一个单调栈来维护。(单调递减)

而这道题目在用单调栈的时候,存储的是元素的下标,也就是你当前在判断的是否要进栈的元素的下标。(我一开始就没弄懂,太坑了。)要注意的是:当一个元素下标出栈的时候,你需要计算他能看到的牛。计算的方法是:需要两个值,一个是当前要进栈的元素的下标,一个是现在要出栈的元素的下标,并且要出栈的元素可能不止一个。而他能看到牛的数量就是两个下标之差-1。最后需要注意的一点是:当所有元素下标都进栈时,需要从栈底开始计算每一个元素能看到牛的数量。

模拟一遍

10 3 7 4 12 2

1.10也就是下标1进栈

2.因为是单调递减的栈,3比10小,所以3也就是下标2进栈

3.这时候7比3大,不符合单调递减,且10又比7大,所以只

3也就是下标2出栈,且7也就是下标3进栈。而因为3也就是下标2出栈了,所以要计算一下:3的下标是2,7的下标是3,所以值为0;

4.栈顶为7,4比7小,所以4也就是下标4进栈

5.这时候12比4、7、10都大,所以这些元素下标都要出栈,

要计算一下:①10的下标是1,12的下标是5,所以值为3;②7的下标是3,所以值为1;③4的下标是4,所以值为5;然后12也就是下标5要进栈

6.最后一个元素2也就是下标6进栈

7.这时候所有元素都进栈了,要进行一波计算:12也就是下标5

是栈底,你可以知道它其实是所有元素里最大的,所以他可以看到所有在他后面的牛,所以它的值就是元素的总数量减去它的下标=1。

然后我就可以敲代码了。

(另外,这道题目当两个数相等的时候是不能算单调的。)

 

 

底下附上代码

#include<bits/stdc++.h>

using namespace std;

int main()

{

long long n,a[80010]={},s[80010]={0,1},top=1,sum=0;

scanf("%lld",&n);

for (int i=1;i<=n;++i)

scanf("%lld",&a[i]);

for (int i=2;i<=n;++i)

if (a[i]<a[s[top]]) s[++top]=i;

else

{

while (a[s[top]]<=a[i]&&top>0)

sum+=i-s[top--]-1;

s[++top]=i;

}

while (top>0)

sum+=n-s[top--];

cout<<sum<<endl;

return 0;

}

这个呢也是书上的板子,我们来理解一下

输入;由于在定义s数组时已经命令s[1]=1,说明下标1已经进栈,所以i从2到n进行循环,每一层循环对每一个下标进行操作。循环里的内容是单调栈的核心,我的写法是我一个思考的过程,中间用了一个if语句,用来判断这个元素下标是否要进栈:如果当前元素值小于栈顶元素值,那么符合单调递减,进栈;否则,就用一个while循环,寻找一下栈前面的元素值有哪些是比它小的,比它小就出栈,并且计算,然后当前元素下标进栈。

而底下那个while是当所有元素都进完栈后,你需要计算当前还在栈内的所有元素(除了最后一个)的值。

 

当然,程序也可以这么写:

#include<bits/stdc++.h>

using namespace std;

int main()

{

long long n,a[80010]={},s[80010]={0,1},top=1,sum=0;

scanf("%lld",&n);

for (int i=1;i<=n;++i)

scanf("%lld",&a[i]);

for (int i=2;i<=n;++i)

{

while (a[s[top]]<=a[i]&&top>0)

sum+=i-s[top--]-1;

s[++top]=i;

}

while (top>0)

sum+=n-s[top--];

cout<<sum<<endl;

return 0;

}

我把中间那个if语句去掉了,因为事实上它并没有什么卵用。

你仔细观察就可以发现那个if语句跟while已经重掉了。

然后这儿还有一个对我来说的难点:top的变化

我可以这样想:进栈就是一句话——s[++top]=i,这里保证的是top是当前的栈顶,它已经存了一个下标,所以top要先++,再让下标进栈;而出栈也是一句话——s[top--],然后对它进行计算,因为如果它不能直接进栈的话,也就意味着前面的元素下标要出栈,而当前的元素下标根本就没有进栈,所以s[top]存的还是之前的元素下标,所以要从当前的s[top]开始while,判断它及它之前的元素下标是否需要出栈,所以它是后自减。

总结起来就是你要判明s[top]当前存的是什么,或者说当前元素下标是否已经进栈。

再来看下一道题:

#213. 地平线

题目描述

Farmer John的牛们认为,太阳升起的那一刻是一天中最美好的,在那时她们 可以看到远方城市模糊的轮廓。显然,这些轮廓其实是城市里建筑物模糊的影子。建筑物的影子实在太模糊了,牛们只好把它们近似地看成若干个边长为1单位 长度的正方体整齐地叠在一起。城市中的所有建筑物的影子都是标准的矩形。牛们 的视野宽W个单位长度(1<=W<=1,000,000),不妨把它们按从左到右划分成W列,并 按1~W编号。建筑物的轮廓用N组(1<=N<=50,000)数给予描述,每组数包含2个整数 x、y(1<=x<=W,0<=y<=500,000),表示从第x列开始,建筑物影子的高度变成了y。(也就是说,第x[i]列到第x[i+1]-1列中每一列建筑物影子的高度都是y[i]个单位 长度)

贝茜想知道这座城市里最少有多少幢建筑物,也就是说,这些影子最少可以由多少个矩形完全覆盖。当然,建筑物的影子可以有重叠。请你写一个程序帮她计算一下。

输入格式

1行: 2个用空格隔开的整数,N和W

2..N+1行: 每行包括2个用空格隔开的整数x、y,其意义如题中所述。输入中的x        严格递增,并且第一个x总是1。

输出格式

1行: 输出一个整数,表示城市中最少包含的建筑物数量

样例数据

input

10 26

1 1

2 2

5 1

6 3

8 1

11 0

15 2

17 3

20 2

22 1

output

6

附上图

 

其实如果你看图的话,不难发现,题目的意思就是找一个矩形,它的最左端和最右端都分别比它左边和右边还要高。

所以你就会发现这可以用一个单调栈来维护。(单调递增)

思路跟乱头发一样,但计算的方法有点差异。

只是这道题目当两个数相等的时候需要覆盖,也就是说:如果楼房的高度是一样的,它是不用sum++的,因为它不是局部最大值。

这道题它要求的是局部最大值。首先,用单调递增的栈来维护,就保证了前面的元素比它小;其次,当它碰到一个比它小的数,这样就找到了局部最大值。而你可以想一下,当楼房都一样高的时候,他自然就不算是局部最大值了,所以这需要覆盖处理一下。

附上代码

#include<bits/stdc++.h>

using namespace std;

int main()

{

int n,w,x,a[50010]={},s[50010]={0,1},top=1,sum=0;

scanf("%d%d",&n,&w);

for (int i=1;i<=n;++i)

scanf("%d%d",&x,&a[i]);

for (int i=2;i<=n;++i)

{

while (a[s[top]]==a[s[top-1]]&&top>0)

top--;

while (a[i]<a[s[top]]&&top>0)

{

top--;

sum++;

}

s[++top]=i;

}

while (top>0)

{

top--;

sum++;

}

cout<<sum<<endl;

return 0;

}

这个程序,我在循环里多加了一个while,就是覆盖了一样的值,

随后一旦找到一个局部最大值,就sum++。后面那个while语句是当所有元素都进过栈了,栈内还有多少元素,sum就加几。

 

 

 

 

 

其实代码也可以这么写(这是我一开始的思路):

#include<bits/stdc++.h>

using namespace std;

int main()

{

int n,w,x,a[50010]={},s[50010]={0,1},top=1,sum=0;

scanf("%d%d",&n,&w);

for (int i=1;i<=n;++i)

scanf("%d%d",&x,&a[i]);

if (n==1&&a[1]==0)

{

cout<<"0";

return 0;

}

a[n+1]=0;

for (int i=2;i<=n+1;++i)

{

while (a[s[top]]==a[s[top-1]]&&top>0)

top--;

while (a[i]<a[s[top]]&&top>0)

{

top--;

sum++;

}

s[++top]=i;

}

cout<<sum<<endl;

return 0;

}

这里我是多增加了一个a[n+1]为0,然后让i循环到n+1,这样的目的是让题中的所有元素处理完后,因为它后面还有一个0,不符合单调递增,这样自然栈内的元素都会使得sum++,这样就少了后面的while。

 

 

 

#215. 子序列累加和

题目描述

x在学习数列。他想到一个数学问题:

现在有N个数的数列。现在你定义一个子序列是数列的连续一部分,子序列的值是这个子序列中最大值和最小值之差。

给你这N个数,小x想知道所有子序列的值得累加和是多少。

输入格式

第一行一个整数N (2 ≤ N ≤ 300 000) 接下来N行,每行一个整数Ai,表示数列的值,保证0<Ai<= 100000 000

输出格式

一个整数,所求的子序列值得累加和。

样例数据

input

4

3

1

7

2

output

31

此题为四个乱头发节。xixi

因为主要是一个子序列是数列的连续一部分,而这个子序列的长度是不确定的,如果用暴力枚举的话你可以想想看,绝对会炸,有那么多个不等长的子序列嘞。其次,题目中说求最大值和最小值之差,所以就考虑用单调栈来求出每一个数在多少个区间中是最大值,在多少个区间中又是最小值。因此,就是求每一个数,它的左边有多少个连续比它小的数,它的右边又有多少个连续比它小的数,然后可以算出这个区间总数。再换句话说,用单调递减的栈倒序求出左边有多少比它小的数,再同样用正序求出右边有多少比它小的数,那么这个数就是这些区间中的最大值;同理,用单调递增的栈倒序求出左边有多少比它大的数,再同样用正序求出右边有多少比它大的数,那个这个数就是这些区间中的最小值。

代码下:

#include<bits/stdc++.h>

using namespace std;

Long long n,a[300010],max1[300010],max2[300010],min1[300010],min2[300010],sum=0;

void leftmax()

{

int s[300010]={},top=1;

s[1]=n;

for (int i=n-1;i>=1;--i)

{

while (a[s[top]]<=a[i]&&top>0)

{

max1[s[top]]=s[top]-i-1;

top--;

}

s[++top]=i;

}

while (top>0)

{

max1[s[top]]=s[top]-1;

top--;

}

}

void rightmax()

{

int s[300010]={},top=1;

s[1]=1;

for (int i=2;i<=n;++i)

{

while (a[s[top]]<a[i]&&top>0)

{

max2[s[top]]=i-s[top]-1;

top--;

}

s[++top]=i;

}

while (top>0)

{

max2[s[top]]=n-s[top];

top--;

}

}

void leftmin()

{

int s[300010]={},top=1;

s[1]=n;

for (int i=n-1;i>=1;--i)

{

while (a[s[top]]>=a[i]&&top>0)

{

min1[s[top]]=s[top]-i-1;

top--;

}

s[++top]=i;

}

while (top>0)

{

min1[s[top]]=s[top]-1;

top--;

}

}

void rightmin()

{

int s[300010]={},top=1;

s[1]=1;

for (int i=2;i<=n;++i)

{

while (a[s[top]]>a[i]&&top>0)

{

min2[s[top]]=i-s[top]-1;

top--;

}

s[++top]=i;

}

while (top>0)

{

min2[s[top]]=n-s[top];

top--;

}

}

int main()

{

scanf("%d",&n);

for (int i=1;i<=n;++i)

scanf("%d",&a[i]);

leftmax();

rightmax();

leftmin();

rightmin();

max1[1]=0;

min1[1]=0;

max2[n]=0;

min2[n]=0;

for (int i=1;i<=n;++i)

sum+=((max1[i]+1)*(max2[i]+1)-(min1[i]+1)*(min2[i]+1))*a[i];

cout<<sum<<endl;

return 0;

}

但是,这道题有坑,有大坑,需要深刻的理解——就是中间两个取等号,两个不取等号的问题。其实到现在我也没有什么很官方的解释,只是你手推一下就可以发现:如果全取等号或全不取等号都是不可以的,都会导致区间总数不对。手推之后可以发现:①如果四个乱头发节里的while全取等号,就会导致一碰到和它相等的元素就被弹出栈,如果是从左往右单调栈,这对相等的数所包含的区间没有被算进去;从右往左的话,这个区间还是没有被算进去。那么区间总数就少了许多。②如果while全不取等号,就会导致包含这对相等数的区间被重复计算,如果是从左往右单调栈,这个区间要算;从右往左的话,还是会算进去。那么区间总数就多了许多。因此从左往右和从右往左总共四次单调栈,一个方向的两个单调栈取等号,另一个方向的两个单调栈不取等号,就保证了所有区间既没有重复计算,也没有漏算。只要把这个理解了,这道题目就能AC了。

原创粉丝点击