题目链接

灌注水箱

题目描述

有一个水箱,从左到右有 个挡板,第 个挡板的高度为 。水从最左侧以每秒 1 个单位体积的速度注入。水在一个区域内的水位是均匀的。当一个区域的水位超过其右侧挡板的高度时,水会溢出到下一个区域。求每个挡板被溢过的最早时刻。

解题思路

直接模拟每一秒的水流过程显然会超时。这是一个动态规划问题,可以通过数据结构进行优化。

1. 核心模型:盆地填充

我们可以将注水过程看作是填充一系列“盆地”。每个挡板 都是其左侧一个盆地的右边界。这个盆地的左边界,则是由左侧第一个高度大于或等于 的挡板 决定的。如果不存在这样的挡板,我们可以认为最左侧有一个无限高的 0 号挡板。

2. 建立递推关系

为恰好将水蓄满到可以溢出第 个挡板(即水位达到 )时,所需要的总注水量。由于注水速度恒定,这个体积在数值上等于所需的时间。

  • 要开始填充以 为右边界的盆地 (L_i, i],必须先完成对 左侧所有盆地的填充,直到水可以溢出 。这部分所需的水量为
  • 当水开始溢出 后,它会流入 (L_i, i] 这个新的区域。这个区域的宽度为
  • 要让水溢出挡板 ,我们需要将这个宽度为 的区域从底部填充到高度 。这需要的新增水量为
  • 因此,总注水量 等于填充到 的水量与填充新区域的水量之和。我们得到递推公式:
  • 我们定义 作为递推的起点。

3. 使用单调栈寻找

问题转化为如何为每个 高效地找到 。这是“寻找左侧第一个更高/相等元素”的经典问题,可以使用单调栈 的时间内解决。
我们维护一个存储挡板下标的栈,并保持栈中下标对应的挡板高度是单调递增的。

4. 最终答案

  • 是在第 秒末,水位恰好达到 所需的总水量/时间。
  • 根据题目定义,水在下一个时间单位,即第 秒,才会“溢过”挡板。
  • 因此,第 个挡板的溢出时刻是

算法步骤

  1. 使用单调栈,从左到右遍历挡板,在 时间内计算出每个挡板 对应的左边界
  2. 根据递推公式 ,在 时间内计算出所有
  3. 输出每个挡板的答案

代码

#include <iostream>
#include <vector>
#include <stack>

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    cin >> n;

    vector<long long> h(n + 1);
    for (int i = 1; i <= n; ++i) {
        cin >> h[i];
    }

    vector<int> left_taller(n + 1, 0);
    stack<int> s;

    for (int i = 1; i <= n; ++i) {
        while (!s.empty() && h[s.top()] <= h[i]) {
            s.pop();
        }
        if (!s.empty()) {
            left_taller[i] = s.top();
        }
        s.push(i);
    }

    vector<long long> v(n + 1, 0);
    for (int i = 1; i <= n; ++i) {
        int l_idx = left_taller[i];
        v[i] = v[l_idx] + h[i] * (i - l_idx);
    }

    for (int i = 1; i <= n; ++i) {
        cout << v[i] + 1 << (i == n ? "" : " ");
    }
    cout << endl;

    return 0;
}
import java.util.Scanner;
import java.util.ArrayDeque;
import java.util.Deque;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        long[] h = new long[n + 1];
        for (int i = 1; i <= n; i++) {
            h[i] = sc.nextLong();
        }

        int[] leftTaller = new int[n + 1];
        Deque<Integer> s = new ArrayDeque<>();

        for (int i = 1; i <= n; i++) {
            while (!s.isEmpty() && h[s.peekLast()] <= h[i]) {
                s.pollLast();
            }
            if (!s.isEmpty()) {
                leftTaller[i] = s.peekLast();
            }
            s.offerLast(i);
        }

        long[] v = new long[n + 1];
        for (int i = 1; i <= n; i++) {
            int lIdx = leftTaller[i];
            v[i] = v[lIdx] + h[i] * (i - lIdx);
        }

        StringBuilder sb = new StringBuilder();
        for (int i = 1; i <= n; i++) {
            sb.append(v[i] + 1).append(i == n ? "" : " ");
        }
        System.out.println(sb.toString());
    }
}
import sys

def solve():
    n_str = sys.stdin.readline()
    if not n_str: return
    n = int(n_str)
    
    h_str = sys.stdin.readline()
    if not h_str: return
    h = [0] + list(map(int, h_str.split()))

    left_taller = [0] * (n + 1)
    s = []

    for i in range(1, n + 1):
        while s and h[s[-1]] <= h[i]:
            s.pop()
        if s:
            left_taller[i] = s[-1]
        s.append(i)

    v = [0] * (n + 1)
    for i in range(1, n + 1):
        l_idx = left_taller[i]
        v[i] = v[l_idx] + h[i] * (i - l_idx)

    ans = [val + 1 for val in v[1:]]
    print(*ans)

solve()

算法及复杂度

  • 算法: 单调栈、动态规划
  • 时间复杂度: 预处理 和计算 都只需要一次线性扫描,因此总时间复杂度为
  • 空间复杂度: 需要额外的数组来存储 ,以及一个单调栈,空间复杂度为