题目描述

✅ 407. 接雨水 II

image-20260928221423437

image-20260928221423438

题意分析

每个格子的宽和长都是 1,积水体积就是水面高度减去地面高度。水可以沿上下左右绕行到地图边缘,因此一个格子能存多少水,取决于通向任意边界的泄水路径,不能只看同一行的左右高墙。

解法:小根堆维护外圈最低边界

核心思路

[!blue]

沿一条路径向外排水,需要越过这条路径上的最高地面;在所有通往外边界的路径中,最高地面最低的那条决定泄水高度。与其为每个格子分别寻找出口,不如从所有外边界一起向内扩展,先处理最低的出口。

小根堆中的每项保存坐标和有效高度 H:它是这个位置已经确定的泄水高度,也是向内扩展时传递的水位。所有外边界都能直接向外排水,所以它们的有效高度就是自身地面高度,且不计积水。

弹出有效高度最小的格子,设一个未访问邻居的地面高度为 h。若 h < H,邻居可以蓄水到 H,新增水量为 H - h,再以水面高度 H 向内扩展;若 h >= H,邻居自身就是更高的阻挡,不接水,之后传递高度 h。合起来,新增水量是 max(0, H - h),入堆的有效高度是 max(H, h)。

为什么第一次发现邻居就能确定水位?此前弹出的格子都检查过四周,所以在本次弹出之前,未访问区域通向外界的路径必然经过堆中的某个格子。本次弹出的 H 是堆内最小值,其他出口不会提供低于 H 的泄水高度;路径又不可能低于邻居自身的地面 h。而经过当前格子的路径恰好达到 max(H, h),因此已经最优,可以立刻标记访问并结算一次。

解题步骤

  1. 行数或列数小于 3 时没有内部格子,返回 0。
  2. 将四条外边界按原始地面高度加入小根堆并标记访问,角点只加入一次。
  3. 弹出有效高度最小的格子,枚举上下左右四个邻居,跳过越界或已访问的位置。
  4. 对未访问邻居立即标记,若其地面低于当前有效高度,就把高度差计入答案。
  5. 取当前有效高度与邻居地面高度的较大值,让邻居携带这个高度入堆。
  6. 堆空时所有格子都已处理,返回累计水量。

代码实现

class Solution {
    private static final int[] ROW_DIRS = {
        1,
        -1,
        0,
        0
    };
    private static final int[] COL_DIRS = {
        0,
        0,
        1,
        -1
    };

    public int trapRainWater(int[][] heightMap) {
        int m = heightMap.length;
        int n = heightMap[0].length;

        if (m < 3 || n < 3) {
            return 0;
        }

        boolean[][] visited = new boolean[m][n];
        PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> Integer.compare(a[2], b[2]));

        for (int row = 0; row < m; row++) {
            addBoundary(heightMap, visited, heap, row, 0);
            addBoundary(heightMap, visited, heap, row, n - 1);
        }

        for (int col = 1; col < n - 1; col++) {
            addBoundary(heightMap, visited, heap, 0, col);
            addBoundary(heightMap, visited, heap, m - 1, col);
        }

        int ans = 0;

        while (!heap.isEmpty()) {
            int[] cur = heap.poll();

            for (int dir = 0; dir < 4; dir++) {
                int nextRow = cur[0] + ROW_DIRS[dir];
                int nextCol = cur[1] + COL_DIRS[dir];

                if (nextRow < 0
                        || nextRow >= m
                        || nextCol < 0
                        || nextCol >= n
                        || visited[nextRow][nextCol]) {
                    continue;
                }

                visited[nextRow][nextCol] = true;

                // 当前最低外壳决定邻居最多能接到多高。
                if (heightMap[nextRow][nextCol] < cur[2]) {
                    ans += cur[2] - heightMap[nextRow][nextCol];
                }

                // 向内传递的是已经形成的有效水位,不能只用原始地面高度。
                heap.offer(
                        new int[] {
                            nextRow,
                            nextCol,
                            Math.max(heightMap[nextRow][nextCol], cur[2])
                        });
            }
        }

        return ans;
    }

    private void addBoundary(
            int[][] heightMap, boolean[][] visited, PriorityQueue<int[]> heap, int row, int col) {
        if (visited[row][col]) {
            return;
        }

        visited[row][col] = true;
        heap.offer(new int[] {
            row,
            col,
            heightMap[row][col]
        });
    }
}
import "container/heap"

func trapRainWater(heightMap [][]int) int {
    m, n := len(heightMap), len(heightMap[0])
    if m < 3 || n < 3 {
        return 0
    }

    visited := make([][]bool, m)
    for row := 0; row < m; row++ {
        visited[row] = make([]bool, n)
    }

    h := &cellHeap{}
    for row := 0; row < m; row++ {
        addRainBoundary(heightMap, visited, h, row, 0)
        addRainBoundary(heightMap, visited, h, row, n-1)
    }
    for col := 1; col < n-1; col++ {
        addRainBoundary(heightMap, visited, h, 0, col)
        addRainBoundary(heightMap, visited, h, m-1, col)
    }

    rowDirs := []int{
        1,
        -1,
        0,
        0,
    }
    colDirs := []int{
        0,
        0,
        1,
        -1,
    }
    ans := 0
    for h.Len() > 0 {
        cur := heap.Pop(h).(cell)
        for dir := 0; dir < 4; dir++ {
            nextRow := cur.row + rowDirs[dir]
            nextCol := cur.col + colDirs[dir]
            if nextRow < 0 || nextRow >= m || nextCol < 0 || nextCol >= n || visited[nextRow][nextCol] {
                continue
            }
            visited[nextRow][nextCol] = true
            // 当前最低外壳决定邻居最多能接到多高。
            if heightMap[nextRow][nextCol] < cur.height {
                ans += cur.height - heightMap[nextRow][nextCol]
            }
            // 向内传递的是已经形成的有效水位,不能只用原始地面高度。
            heap.Push(h, cell{row: nextRow, col: nextCol, height: max(heightMap[nextRow][nextCol], cur.height)})
        }
    }
    return ans
}

type cell struct {
    row    int
    col    int
    height int
}

type cellHeap []cell

func (h cellHeap) Len() int {
    return len(h)
}

func (h cellHeap) Less(i int, j int) bool {
    return h[i].height < h[j].height
}

func (h cellHeap) Swap(i int, j int) {
    h[i], h[j] = h[j], h[i]
}

func (h *cellHeap) Push(value any) {
    *h = append(*h, value.(cell))
}

func (h *cellHeap) Pop() any {
    old := *h
    value := old[len(old)-1]
    *h = old[:len(old)-1]
    return value
}

func addRainBoundary(heightMap [][]int, visited [][]bool, h *cellHeap, row int, col int) {
    if visited[row][col] {
        return
    }
    visited[row][col] = true
    heap.Push(h, cell{row: row, col: col, height: heightMap[row][col]})
}

func max(first int, second int) int {
    if first > second {
        return first
    }
    return second
}

复杂度分析

  • 时间复杂度:$O(mn \log(mn))$,每个格子至多入堆、出堆一次。
  • 空间复杂度:$O(mn)$,用于访问数组和小根堆。

关键点总结

[!green]

  • 一条外流路径由最高地面限制,多条路径中选限制最低的出口。
  • 堆维护的是已经确定的有效水位,每次用最低水位向内扩展。
  • 低洼地填满后,其水面继续承担边界作用,所以邻居以 max(H, h) 入堆。
  • 最低水位优先保证邻居首次被发现时即可定值,入堆时标记使每个格子只计水一次。

易错点总结

[!yellow]

  • 分别按行或列套用一维算法,会忽略水绕行到其他方向出口的可能。
  • 邻居按原始高度入堆,会丢失已经形成的水面高度并低估后续水量。
  • 出堆时才标记访问,可能让同一格从多个方向重复入堆和重复计水。
  • 只放入部分外边界会遗漏泄水出口;当前一次定值的写法也不能直接改用普通队列或大根堆。
  • 入堆高度与积水量不是同一个量:前者取 max(H, h),后者取 max(0, H - h)。

相似题目

题目 难度 关联与区别
42. 接雨水 困难 一维只需左右边界,本题四周都可能漏水,需要从最低外边界逐步向内扩展。
778. 水位上升的泳池中游泳 困难 同样用最小堆维护不断抬高的可达水位,原题求抵达终点所需的最低水位。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2024/34609866
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!