这题实际上是一个单调栈的应用,但还是比较巧妙的。
虽然问的是“互相看见”,但如果
i
和
直接枚举,复杂度显然是 O(n2) 的。期望得分为 40%。但如果采用了“考虑前面”的思想方法,会对后面的解题方法有所帮助理解。
体会一下单调队列的性质,可以认为,大致表现为下图:
对于中间比较低的部分,在后面红色入队的时候就要被迫出队了,因为红色比它高。形象化地想象一下,这像不像题目中所述的“挡住”?
而在本题中,没有对于区间的限制,只是计数问题。因此可以认为就是一个单调栈。我们来维护一个栈中元素自底向上单调不增的栈。
上面的图其实就是对应了样例,一步步来分析。
第一个人身高为 2,它前面没有人,因此答案仍然为 0。同时将其入栈,此时栈中元素为 {2}。
第二个人身高为 4,此时栈顶元素为 2,因为当前比栈顶元素更高,所以 (1,2) 之间一定能互相看到,答案为 1。同时 2 出栈,4 入栈,栈为 {4}。
第三个人身高为 1,此时栈顶元素为 4,因为当前栈不为空且栈顶元素比当前更高,所以 (2,3) 之间能互相看到,答案为 2。1 入栈后,栈为{4, 1}。
第四个人身高为 2,此时栈顶元素为 1,与第二个人时的情况同理, (3, 4) 之间能互相看到,1 出栈,4 比 2 高,停止出栈。此时与第三个人时的情况同理,(2, 4) 之间能互相看到。因此答案为 4。2 入栈后,答案为 {4, 2}。
第五个人身高为 2,此时栈顶元素为 2。当前与栈顶元素相等,(4, 5) 之间能相互看到,答案加 1 但不将其出栈。考虑栈顶元素下方的 4,4 比 2 高,因此 (2, 5) 之间能互相看到。答案为 6。将 2 入栈,此时栈为 {4, 2, 2}。
第六个人身高为 5,此时栈顶元素为 2。与第二个人时的情况同理,(5, 6) 之间能互相看到,2 出栈;(4, 6) 之间能互相看到,2 出栈;(2, 6) 之间能互相看到,5 出栈。因此答案为 9。将 5 入栈,此时栈为 {5}。
第七个人身高为 1,此时栈顶元素为 5。与第三个人时的情况同理, (6, 7) 之间能互相看到,答案为 10。1 入栈,栈为 {5, 1}。
计算结束,答案为 10。
现在要来证明三个东西:
第二个人这种情况。只要当前比栈顶元素更高,那么当前和栈顶元素一定能互相看到。如图:
推理证明:在栈顶元素和当前之间的元素有 3 种情况:
如情况 1,比当前元素和栈顶元素都要低,根据单调不增栈的性质,此时假定的栈顶元素如果还在栈中,一定在它下方。因此这种情况不成立。
如情况 2 和 3,比栈顶元素都要高,可以反证。假设我们假定的这个栈顶元素还在栈中,则违反了单调不增栈的性质。所以此时假定的栈顶元素不可能还出现在栈中,而应该早就出栈了。因此这种情况也不成立。
综上所述,我们认为,只要当前比栈顶元素更高,就一定能和栈顶元素之间相互看到。
第三个人这种情况。只要栈不为空且栈顶元素比当前更高,当前和栈顶元素一定能互相看到。如图:
推理证明(其实和上面很类似):
如情况 1,比当前和栈顶元素都要低,不会造成影响。
如情况 2,比当前和栈顶元素都要高,跟上面的情况 2 同理,可用反证法证明不成立。
如情况 3,比当前高但比栈顶元素低,跟上面的情况 1 同理,由单调栈的性质可证明不成立。
综上所述,只要栈不为空且栈顶元素比当前更高,当前和栈顶元素之间一定能互相看到。
第五个人这种情况。只要栈顶连续的一段和当前相等,就都能和他们看到。利用上面的逻辑可以很快推导出来,比较显然,这里不再证明。
这样我们就证明了利用单调栈解决这题的正确性。
参考代码:
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
using namespace std;
const int MAXN = 5e5 + 100;
int N;
int h[MAXN];
int st[MAXN], top;
int main(void) {
freopen("2116.in", "r", stdin) ;
freopen("2116.out", "w", stdout) ;
scanf("%d", &N);
for (int i = 0; i < N; i++) scanf("%d", &h[i]);
int S = 0; st[++top] = h[0];
for (int i = 1; i < N; i++) {
while (top && st[top] < h[i]) { //身高不同者的处理,注意不能写成 h[st[top].first] <= h[i]
--top;
++S;
}
int k = top;
while (k && st[k] == h[i]) { //身高相同者的处理,ans 加 1 但不删除队尾元素
--k;
++S;
}
if (k) ++S; //队不空则继续加 1
st[++top] = h[i]; //入队
}
printf("%d\n", S);
return 0;
}
时间复杂度?你也许会脱口而出: O(n) 。但真的是这样吗?
因为我们维护的是一个单调不增栈,并不是严格递减,允许出现相等的一段。如果所有人身高都相同,那么每次都要考察前面所有跟它相同身高的人。
也就意味着,将会退化到 O(n2) 的级别!
幸运的是,我们不难找到解决方法。
仍然维护一个单调不增栈,在处理相同元素时,可以利用栈中元素的单调性,在栈中进行二分查找,快速找出跟当前相等的人数。复杂度为 O(nlogn) 。
改为维护一个严格的单调递减栈,并将每个元素记 (value,count) 以储存连续的一段相等元素。
但要注意的是,对于当前比栈顶元素高的情况,出栈时答案加上出栈元素的 count ;但对于栈顶元素比当前高的情况,只能加 1!
这是因为:即便当前栈顶是连续的几个身高相同的人,当前也只能看到最后一个人。跟之前的人,因为当前比它们低,所以被挡住了,并不能看到。
这样一来,就可以在总的 O(n) 时间内完美解决此题。
参考代码:
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
using namespace std;
typedef pair <int, long long> pil; //前一个是编号,后一个是人数
const int MAXN = 5e5 + 100;
int N;
int h[MAXN];
pil st[MAXN];
int top;
int main(void) {
freopen("2116.in", "r", stdin) ;
freopen("2116.out", "w", stdout) ;
scanf("%d", &N);
for (int i = 0; i < N; i++) scanf("%d", &h[i]);
long long S = 0; st[++top] = make_pair(0, 1LL);
for (int i = 1; i < N; i++) {
while (top && h[st[top].first] < h[i]) //身高不同者的处理
S += st[top--].second;
int k = top;
if (h[st[k].first] == h[i]) S += st[k--].second; //与自己同身高的都能看见
if (k) ++S;
if (h[st[top].first] == h[i]) ++st[top].second; //与栈顶相同只需将其人数累加
else st[++top] = make_pair(i, 1LL); //向栈中压入新元素
}
printf("%lld\n", S);
return 0;
}