【单调栈】-----【Bad Hair Day S】

P2866 [USACO06NOV] Bad Hair Day S

注意:洛谷题面翻译有误,原文是“height ≥”,即每头牛可以看到前方所有身高小于等于自己的连续牛。本题解已据此修正。
题目链接

题目描述

农夫约翰有 N N N 头奶牛正在过乱头发节。

每一头牛都站在同一排面朝右,它们被从左到右依次编号为 1 , 2 , ⋯   , N 1, 2, \cdots, N 1,2,,N。编号为 i i i 的牛身高为 h i h_i hi。第 N N N 头牛在最前面,而第 1 1 1 头牛在最后面。

对于第 i i i 头牛前面的第 j j j 头牛,如果 h i ≥ h i + 1 , h i ≥ h i + 2 , ⋯   , h i ≥ h j h_i\ge h_{i+1}, h_i\ge h_{i+2}, \cdots, h_i\ge h_j hihi+1,hihi+2,,hihj,那么认为第 i i i 头牛可以看到第 i + 1 i+1 i+1 到第 j j j 头牛。

定义 C i C_i Ci 为第 i i i 头牛所能看到的牛的数量。请帮助农夫约翰求出 C 1 + C 2 + ⋯ + C N C _ 1 + C _ 2 + \cdots + C _ N C1+C2++CN

输入格式

输入共 N + 1 N + 1 N+1 行。

第一行为一个整数 N N N,代表牛的个数。
接下来 N N N 行,每行一个整数 a i a _ i ai,分别代表第 1 , 2 , ⋯   , N 1, 2, \cdots, N 1,2,,N 头牛的身高。

输出格式

输出共一行一个整数,代表 C 1 + C 2 + ⋯ + C N C _ 1 + C _ 2 + \cdots + C _ N C1+C2++CN

输入输出样例 #1

输入 #1

6
10
3
7
4
12
2

输出 #1

5

说明/提示

数据规模与约定

对于 100 % 100\% 100% 的数据,保证 1 ≤ N ≤ 8 × 10 4 1 \leq N \leq 8 \times 10 ^ 4 1N8×104 1 ≤ h i ≤ 10 9 1 \leq h _ i \leq 10 ^ 9 1hi109


题解(单调栈+非严格比较)

单调栈原理见本文

一、问题抽象

题目本质是要我们找出:每头牛向右看,最多能看到几头“连续不比它高”的牛,一旦遇到第一头比它高的牛,视线就终止。

这类“向右看”、“遇到第一个满足某条件的位置”问题,属于经典的 “下一个更大元素” 问题,可以用 单调栈 来高效求解。


二、算法设计

➤ 单调栈的定义与使用:
  • 我们从右往左遍历奶牛序列。

  • 使用一个单调递减栈维护“当前可见的右侧牛的下标”:

    • 栈中存的是下标;
    • 栈内元素对应身高 从栈顶到栈底单调递减
    • 每次遍历位置 i i i

1. 把所有高度 < h i h_i hi 的栈顶元素弹出;

2. 剩下的栈顶元素即为第一个右边比它高的牛;

  • 记录该位置为 ender[i],表示第 i i i 头牛视线终止的位置
  • 若栈空,则说明 i i i 后面所有牛都能看到,记为 n+1(越界标记)

3. 当前下标 i i i 入栈,供更左边的牛判断用。

➤ 计算可见数量:
  • 对于每个 i i i,能看到的是区间 ( i , ender [ i ] ) (i, \text{ender}[i]) (i,ender[i]) 的所有牛;
  • 所以可见数量为: ender [ i ] − i − 1 \text{ender}[i] - i - 1 ender[i]i1
  • 这里注意栈空时的 越界标记 十分关键
  • 累加所有 i i i 的结果即可。

三、样例模拟分析

以样例输入:

6
10
3
7
4
12
2

遍历过程中每个牛的 ender[i] 为:

牛编号 i i i h i h_i hi 右边第一个更高的牛位置 ender[i] 可见数量 ender[i]-i-1
1 10 5 3
2 3 3 0
3 7 5 1
4 4 5 0
5 12 7 1
6 2 7 0

总和为 3 + 0 + 1 + 0 + 1 + 0 = 5 3 + 0 + 1 + 0 + 1 + 0 = 5 3+0+1+0+1+0=5,输出正确。


四、完整代码:

#include 
using namespace std;
#define int long long  

signed main()
{
    int n;
    
    // 输入奶牛数量(从左到右为第 1 头到第 n 头)
    cin >> n;  
	
	// 下标从 1 开始,nums[i] 表示第 i 头牛的身高
    vector<int> nums(n + 1); 

    for (int i = 1; i <= n; i++)
    {
    	// 输入每头牛的身高
        cin >> nums[i];  
    }
	
	// 单调栈(维护身高单调递减的下标序列):
    // 用于寻找每头牛右边第一个“高度 ≥ 它”的牛的下标(即视线被阻挡的位置)
    stack<int> stk;

    
    // ender[i] 表示第 i 头牛能看到的最远牛的下标(不含该下标)
    vector<int> ender(n + 1);  

    // 从右往左处理:因为牛面朝右,所以我们从最右侧(编号 n)开始向左处理
    for (int i = n; i >= 1; i--)
    {
        // 弹出所有比当前牛矮的牛:这些牛会被当前牛挡住,无法成为其他牛能看到的目标
        // 当前牛只能看到它右边连续的“比它矮”或“与它等高”的牛,一旦遇到更高的牛,视线就被阻挡
        while (!stk.empty() && nums[stk.top()] < nums[i])
        {
	        // 栈顶牛比当前牛矮,会被当前牛挡住,移除
            stk.pop();  
        }

        // 如果栈为空,说明右边没有任何牛能阻挡当前牛的视线
        // 它可以看到直到最右边(n+1 相当于越界标记,很关键的技巧)
        // 否则,栈顶的牛是第一个比当前牛高或一样高的,它阻挡了视线
        ender[i] = stk.empty() ? (n + 1) : stk.top();

        // 当前牛入栈:作为它左边牛判断“视线终止点”的候选对象
        stk.push(i);
    }

    int ans = 0;  // 累计所有牛能看到的牛的总数

    for (int i = 1; i <= n; i++)
    {
        // 当前牛能看到的范围是 [i+1, ender[i]-1]
        // 一共 ender[i] - i - 1 头牛
        // 注意:之前记录答案时的越界标记很重要
        ans += (ender[i] - i - 1);
    }
	
	// 输出所有牛能看到的牛数总和
    cout << ans;  

    return 0;
}


四、复杂度分析

操作 复杂度
遍历所有牛(从右向左) O ( N ) O(N) O(N)
每个牛最多入栈一次,出栈一次 O ( N ) O(N) O(N)
最终总和计算 O ( N ) O(N) O(N)
总时间复杂度 O ( N ) O(N) O(N)
空间复杂度 O ( N ) O(N) O(N)(用于存栈和辅助数组)

六、总结

  • 本题本质是典型的““找右边第一个大于等于当前值的位置””的题型;
  • 单调栈是一种非常高效的数据结构,适用于一类 “最近比我高/低的数” 问题;
  • 注意栈中维护的是下标,实际比较的是身高;
  • 遇到类似“能看到多少连续满足某条件的右边元素”的问题,优先考虑单调栈!

你可能感兴趣的:(栈,算法,栈,单调栈)