先放一道题目:
#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了。