[SMOJ2116]诺诺的队列

本文通过分析SMOJ2116题目的解题思路,讲解如何运用单调栈解决‘互相看见’的问题。通过示例详细解释了如何在不同身高情况下判断是否能互相看到,并通过单调栈的性质进行证明。最后讨论了处理相同身高时的时间复杂度优化,提出利用栈中元素的单调性进行二分查找,以实现O(nlogn)的复杂度解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这题实际上是一个单调栈的应用,但还是比较巧妙的。

虽然问的是“互相看见”,但如果 i j 都从 1 开始枚举,显然会造成不必要的重复计数。不妨采用一种常用的策略,即只考虑能多少人看到前面多少人。

直接枚举,复杂度显然是 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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值