USACO 2022 Feb Silver

Cow Frisbee

基于单调栈的 O(N) 解法

1. 理解题目

输入: $N$ 头牛排成一列,每头牛高度 $h_i$ 不同。

投掷条件: 牛 $i$ 和牛 $j$ 能互相投飞盘,当且仅当:

  • 它们中间的所有牛都比 $h_i$ 和 $h_j$ 矮。

目标: 计算所有可行对 $(i, j)$ 的距离总和。

$$ \text{Distance} = |i - j| + 1 $$

注意:题目中的距离公式是 $j-i+1$,即索引差+1。

2. 关键性质分析

对于任意一头牛 $i$,谁是能和它扔飞盘的最远的牛?

  • 看:它是第一头比 $i$ 高的牛。
  • 看:它是第一头比 $i$ 高的牛。

为什么?

如果中间有比 $i$ 高的牛,挡住了视线,无法投掷。
如果中间有比 $target$ 高的牛,挡住了视线,也无法投掷。

$\Rightarrow$ 问题转化为:
寻找每头牛左右两侧“第一个比它高”的牛。

朴素解法 vs 优化

  • 朴素做法 $O(N^2)$:
    枚举每一对 $(i, j)$,检查中间最大值。
    超时!$N \le 3 \times 10^5$
  • 优化思路:
    我们需要一种数据结构,能够快速维护“当前还没找到比自己高的人”的集合。
  • 单调栈 (Monotonic Stack)
    维护一个高度单调递减的栈。

算法流程

  1. 遍历每头牛 $i$ (高度 $h[i]$)。
  2. 检查栈顶
    • 如果 $h[\text{top}] < h[i]$:说明 $h[i]$ 是栈顶牛右边第一个比它高的。
      $\rightarrow$ 配对成功! 累加距离,弹出栈顶。
    • 持续弹出,直到栈为空或栈顶比 $h[i]$ 高。
  3. 检查剩余栈顶
    • 如果栈不为空 ($h[\text{top}] > h[i]$):说明栈顶是 $h[i]$ 左边第一个比它高的。
      $\rightarrow$ 配对成功! 累加距离。
  4. 将 $i$ 入栈。

可视化演示

准备开始...
0/0

示例数据: [3, 1, 4, 2, 5]

核心代码实现


#include <cstdio>
#include <stack>
#include <vector>
using namespace std;
typedef long long ll;

int n;
ll ans;
int a[300005];
stack<int> s; // 存储索引

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%d", &a[i]);

    for (int i = 0; i < n; i++) {
        // 1. 右侧比栈顶高:栈顶找到右边界
        while (!s.empty() && a[s.top()] < a[i]) {
            ans += i - s.top() + 1;
            s.pop(); 
        }
        // 2. 栈顶比当前高:当前牛找到左边界
        if (!s.empty()) ans += i - s.top() + 1;
        s.push(i);
    }
    printf("%lld\n", ans);
    return 0;
}
                

为什么一次遍历就够了?

代码中巧妙地在一个循环里处理了两种情况:


Case 1: While 循环内

a[s.top()] < a[i]

栈顶元素比较矮,它遇到了右边第一个比它高的牛 $i$。

$\rightarrow$ 计算的是以 `s.top()` 为左端点,`i` 为右端点的贡献。


Case 2: While 循环后

!s.empty() (意味着 a[s.top()] > a[i])

当前牛 $i$ 比较矮,它看到了左边第一个比它高的牛 `s.top()`。

$\rightarrow$ 计算的是以 `s.top()` 为左端点,`i` 为右端点的贡献。

复杂度分析

  • 时间复杂度:$O(N)$
    • 虽然有两层循环(for + while),但每个元素最多进栈一次,出栈一次。
    • 操作次数是线性的。
  • 空间复杂度:$O(N)$
    • 栈在最坏情况下(递减序列)可能存储 $N$ 个元素。

总结

  • 题目本质是找“最近的大于值”。
  • 单调栈是解决此类问题的利器。
  • 利用 while 循环处理出栈(右边界),利用剩余栈顶处理入栈(左边界)。
  • 注意结果需要使用 long long

AC!