LeetCode 接雨水系列 42.(一维) 407.(二维)

42.接雨水

题目描述

给定n个非负整数表示每个宽度为1的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水

思路

依次看每根柱子能接多少雨水。
某根柱子能接的雨水,取决于该柱子左侧的最高柱子leftMax,和右侧的最高柱子rightMax。根据木桶效应,能接的水取决于左右两侧最高柱子的较小者,即h = min(leftMax,rightMax),若h比当前柱子大,则当前柱子能接到的雨水。那么一个比较直观的思路就是,我们先预处理出每个位置的左侧最大值和右侧最大值,然后再遍历一次,在每个柱子的位置接雨水即可。

class Solution {
    public int trap(int[] height) {
        int n = height.length;
        int[] leftMax = new int[n];
        int[] rightMax = new int[n];
        leftMax[0] = height[0];
        rightMax[n - 1] = height[n - 1];
        
        for (int i = 1; i < n; i++) {
            leftMax[i] = Math.max(leftMax[i - 1], height[i]);
        }

        for (int i = n - 2; i >= 0; i--) {
            rightMax[i] = Math.max(rightMax[i + 1], height[i]);
        }

        int ans = 0;
        for (int i = 0; i < n; i++) {
            int h = Math.min(leftMax[i], rightMax[i]);
            if (h > height[i]) ans += h - height[i];
        }
        return ans;
    }
}

上面两次预处理,加一次遍历计算,总共花费的时间为3n,其实可以优化为2n,我们可以先预处理出rightMax,然后,再边维护leftMax,边接雨水。

class Solution {
    public int trap(int[] height) {
        int n = height.length;
        int[] rightMax = new int[n];
        rightMax[n - 1] = height[n - 1];

        for (int i = n - 2; i >= 0; i--) {
            rightMax[i] = Math.max(rightMax[i + 1], height[i]);
        }

        int ans = 0, leftMax = height[0];
        for (int i = 1; i < n; i++) {
            leftMax = Math.max(leftMax, height[i]);
            int h = Math.min(leftMax, rightMax[i]);
            if (h > height[i]) ans += h - height[i];
        }
        return ans;
    }
}

实际上,我们还可以用双指针来进行优化,只需要遍历一次即可算出答案。

核心的思路是:用2个指针,i从左往右移动,j从右往左移动。我们能够在过程中正确维护i的左侧最大值,和j的右侧最大值,然后我们每次计算在i或者j处的接雨水的量。

我们用iLeftMax表示位置i处左侧的最高柱子高度,iRightMax表示i右侧最高的柱子高度。jLeftMaxjRightMax同理。

我们在双指针的遍历过程中,能够得到iLeftMaxjRightMax的值。

由于ij的左侧,我们容易得知如下关系:iLeftMax <= jLeftMax。因为j左侧的最大值,要么是i左侧的最大值,要么是ij之间出现了比iLeftMax更大的值。

同样,容易得到:iRightMax >= jRightMax,因为ij要多考虑区间[i,j]iRightMax只可能比jRightMax更大。

整理一下,我们有:

iLeftMax <= jLeftMax
iRightMax >= jRightMax

并且我们能获取到iLeftMaxjRightMax的值。

iLeftMax <= jRightMax时,根据上面的关系,有iLeftMax <= jRightMax <= iRightMax,进而有iLeftMax <= iRightMax,此时位置i能接的雨水,只取决于iLeftMax,而iLeftMax我们是已知的,所以此时可以计算i处的雨水。

iLeftMax > jRightMax时,根据上面的关系,有jLeftMax >= iLeftMax > jRightMax,进而有jLeftMax > jRightMax,此时位置j能接的雨水,只取决于jRightMax,而jRightMax我们是已知的,所以此时可以计算j处的雨水。

双指针优化后,只需要一趟遍历即可完成计算。

class Solution {
    public int trap(int[] height) {
        int n = height.length;
        int leftMax = height[0], rightMax = height[n - 1];
        int i = 1, j = n - 2;
        // iLeftMax <= jLeftMax
        // iRightMax >= jRightMax
        int ans = 0;
        while (i <= j) {
            if (leftMax <= rightMax) {
                // iLeftMax <= jRightMax <= iRightMax
                leftMax = Math.max(leftMax, height[i]);
                ans += leftMax - height[i];
                i++;
            } else {
                // jLeftMax >= iLeftMax > jRightMax
                rightMax = Math.max(rightMax, height[j]);
                ans += rightMax - height[j];
                j--;
            }
        }
        return ans;
    }
}

其余解法:单调栈。

换个角度考虑,什么时候某个位置可能接到雨水呢,是不是只有柱子高度先下降,后上升,形成一个 “盆地”时(一个V字形),在中间的低洼处才能接到水呢?于是我们考虑用单调栈。栈里面存的是高度递减的柱子,从左往右遍历的时候,只要遇到一个比栈顶的柱子高度大的,则说明能和前面的柱子形成一个低洼处,能够接到雨水。

class Solution {
    public int trap(int[] height) {
        int n = height.length;
        int[] stack = new int[n];
        int top = -1; // 栈顶
        int ans = 0;

        for (int i = 0; i < n; i++) {
            // 栈非空, 且栈顶的柱子高度小于当前柱子, 则开始接雨水
            while (top >= 0 && height[stack[top]] < height[i]) {
                int j = stack[top--]; // 准备接j这个位置的雨水
                if (top < 0) break; // 没有前一个柱子了, 无法接j处的雨水
                int k = stack[top]; // k和i围住了j, 可以接j处雨水, 注意不要把k也弹出栈
                int minH = Math.min(height[k], height[i]);
                ans += (minH - height[j]) * (i - k - 1);
            }
            // 该柱子能和前面的柱子接的雨水, 计算完毕, 插入该柱子
            stack[++top] = i;
        }
        return ans;
    }
}

407.接雨水II

题目描述:

给你一个 m x n 的矩阵,其中的值均为非负整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。

上一道题是一维的,这道题是二维的。

思路

第一次见这道题时,不太容易想清楚 [满足什么条件才能接到雨水] 这一点。比较直觉但是错误的想法是,对于某个位置[i,j],找到其上下左右4个方向上,高度最大的4根柱子,取4根柱子中高度最小的,再和当前这跟柱子比较高度。

这样是错误的,水还可能从其他边边角角的地方流走。一种相对正确但是不太好想象的想法是:对于[i,j],找到其四周围成一圈的一堵墙,这堵墙中,高度最小的柱子,再和[i,j]的柱子比高度。但这种想法很难付诸实践。

我们换个角度来看,当下完雨后,每个柱子有个最终高度,若这个最终高度大于该柱子原有高度,则说明在这个柱子的位置,接到了雨水。我们用f(i,j)来表示在位置[i,j]处的柱子的最终高度。引入这个最终高度的概念后,就很好想了。

f(i,j),其实就取决于其周围4个相邻位置的最终高度。容易得到状态转移方程如下:

f(i,j) = max{ min{f(i-1, j), f(i + 1, j), f(i, j - 1), f(i, j + 1)}, height[i][j] }

即,位置[i,j]的柱子的最终高度,为其4个相邻位置的最终高度的最小值,再和[i,j]柱子原本的高度取一个max。

然后是状态的边界。容易得知,整个矩形最外层的柱子,都是接不到水的,水一定会从边界流出去。所以对所有最外层的柱子,初始化其f(i,j) = height[i][j]。我们每次,从已经求得f(i,j)的柱子中,找到f(i,j)值最小的,并更新其相邻的,还未求出f(i,j)的柱子。

为什么这样能求出正确答案。首先,根据木桶效应,f(i,j)只取决于其相邻的4个位置的最小者。所以我们每次从已经确定f(i,j)的柱子中,找出值最小的。假设我们选中的f(i,j)最小的柱子是a。那么, 与柱子a相邻的柱子,其f(i,j)一定是由柱子a转移过来的吗?

答案是肯定的,假设与柱子a相邻的某个柱子b,其还有其他3个相邻的柱子,若这3个相邻的柱子的最终高度已经求出,而我们选择的最小值为柱子a,说明这3个相邻柱子的最终高度都大于柱子a的。若这3个相邻的柱子里,有些柱子的最终高度还没有求出,则这些还没有求出最终高度的柱子,其最终高度,都是由已经求出最终高度的某根柱子转移过来的,而状态转移的过程中,f(i,j)只可能增大,不可能变小,所以这些没有求出最终高度的柱子的最终高度,一定是大于其状态转移链的初始那个状态,而这个初始状态一定是此时f(i,j)已知的,而此时已知的f(i,j)中,柱子a是最小的。也能够推出,其他3个相邻柱子的最终高度都一定大于a。所以柱子b的最终高度,一定是由柱子a的状态更新过来的。

再换个角度想,我们将最外层的柱子的状态f(i,j)进行了初始化,相当于在最外圈,围起了一堵墙(边界),这堵墙中间的某根柱子,是否能接到水,一定取决于最外圈这堵墙中(边界上),高度最小的那根柱子。所以我们每次需要取高度最小的。

于是,我们的算法就出来了。其实和Dijkstra特别像。每次从f(i,j)已知的柱子中,选择一个最小的,更新其相邻的柱子。我们用小根堆来做。

class Solution {

    class Node {
        int x;
        int y;
        int h; // 下雨之后该柱子的最终高度

        Node(int x, int y, int h) {
            this.x = x;
            this.y = y;
            this.h = h;
        }
    }

    public int trapRainWater(int[][] heightMap) {
        int n = heightMap.length, m = heightMap[0].length;
        boolean[][] st = new boolean[n][m]; // 用来标记那些已经算出最终高度的柱子
        // 按最终高度从小到大
        PriorityQueue<Node> heap = new PriorityQueue<>((o1, o2) -> o1.h - o2.h);
        // 将矩阵最外层的柱子插进堆
        for (int i = 0; i < m; i++) {
            heap.offer(new Node(0, i, heightMap[0][i])); // 第一行
            heap.offer(new Node(n - 1, i, heightMap[n - 1][i])); // 最后一行
            st[0][i] = st[n - 1][i] = true;
        }
        // 重复元素不影响最终答案
        for (int i = 0; i < n; i++) {
            heap.offer(new Node(i, 0, heightMap[i][0])); // 第一列
            heap.offer(new Node(i, m - 1, heightMap[i][m - 1])); // 最后一列
            st[i][0] = st[i][m - 1] = true;
        }
        int ans = 0;

        int[] dx = {1, -1, 0, 0};
        int[] dy = {0, 0, 1, -1};

        while (!heap.isEmpty()) {
            Node node = heap.poll();
            for (int i = 0; i < 4; i++) {
                int nx = node.x + dx[i];
                int ny = node.y + dy[i];
                if (nx < 0 || nx >= n || ny < 0 || ny >= m || st[nx][ny]) continue; // 越界或已求出最终高度
                ans += Math.max(node.h, heightMap[nx][ny]) - heightMap[nx][ny]; // 接雨水
                heap.offer(new Node(nx, ny, Math.max(node.h, heightMap[nx][ny])));
                st[nx][ny] = true; // 该柱子已求出最终高度
            }
        }
        return ans;
    }
}

上面的思路是我自己看了yxc的视频讲解后,总结的一种比较简单,方便记忆和理解的思路。
yxc的原思路,参考这个链接。其证明和推导过程十分严谨,但不是特别容易理解。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值