横向归一 : 42. 接雨水 与 135.分发糖果 --刷leetcode 随笔

180 阅读9分钟

前言

夜已深 , 耳机里萦绕着 程响的歌曲《在他乡》, 我贸然敲下这篇文章 , 敬各位深夜孤勇者 ~

看了很多算法博客 , 很多都是在搞一题多解 , 就是在深度上做文章 , 笔者算法小菜鸡 , 就不在这与大佬们百家争鸣了 。(后续还会在深度上做文章,经典的题目总是要多刷几遍的😀)

笔者将带来两道看似"老死不相往来"的题目 , 让他们联姻 !

也许有倔友以为 "横向归一" 是题目跳跃游戏和跳跃游戏 II 般"天造地设" , 其实不是 , 他们是"有缘千里来相会" , 所谓"金风玉露一相逢,便胜却人间无数" !

请各位倔友往下看

42. 接雨水

leetcode.cn/problems/tr…

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

示例 1:

  • 输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
  • 输出:6
  • 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。

示例 2:

  • 输入:height = [4,2,0,3,2,5]
  • 输出:9

这道题目的核心点在于如何求雨水的面积

Carl 大佬提供了两种思路 , 我觉得这是解题最重要的切入点 , 要认真思考

  • 按列来计算水的面积
  • 按行来计算水的面积

个人认为按列计算是最容易实现的思路 ,我们具体看看什么是按列计算水的面积


如上图 : 把雨水的面积分成 1-5 的长方形 , 这样就是按列来计算雨水的面积 !

对于这样的一个面积也很好求 , 因为有一个巧合是 , 每一列的宽度是 1 , 也就是说 ,只需要求出水柱的高度 ,

就可以求出该列的面积 ,那么问题来了 ,如何求高度 ?


求高度 , 那么就必须搞明白 , 这个高度是由什么决定的 !

看水柱 1 , 他的高度取决于左右高度(h1 和 h2) , 左边 h1 短 , 所以取决于短的 h1 。

看水柱 2 ,他的高度取决于左边最高 h2 , 右边最高 h5 , 按水柱 1 的想法来就是取决于 h2 (h2<h5) , 然而不要忘记 h3 的高度要减去 , 所以到这里就可以推出: 水柱高度取决于当前要求的水柱位置 , 他左边黑柱子最高高度 ,与他右边黑柱子最高高度 , 还有当前位置黑柱子的高度 !

自变量都找齐了 , 因变量不是手撕吗 ?🤡

即枚举每一个位置 i ,height[i] 就是当前黑柱子高度 ,

  • 设 i 位置左边黑柱子最高为 maxLeft[i] ,
  • 设i 位置右边黑柱子最高为 maxRight[i],
  • 设通过上面自变量求出的因变量(水柱高度)为 ans[i]

从而根据前面分析可知 : ans[i] =min(maxLeft[i] ,maxRight[i])-height[i]

那么所有的水柱子就是 : 设 sum=0 , 在遍历一遍数组过程中收集 ,sum+=ans[i] (i : 0-height.length-1)

那么问题又来了 , 如何求出每个位置的maxLeft ,maxRight ?


有过算法经验的 , 这个也好想到

  • 左遍历求每个位置的maxLeft
  • 右遍历求每个位置的maxRight

求 maxLeft

  // 记录每个柱子左边柱子最大高度
        maxLeft[0] = height[0];
        for (int i = 1; i < size; i++) {
            maxLeft[i] = max(height[i], maxLeft[i - 1]);
        }

求maxRight

   // 记录每个柱子右边柱子最大高度
        maxRight[size - 1] = height[size - 1];
        for (int i = size - 2; i >= 0; i--) {
            maxRight[i] = max(height[i], maxRight[i + 1]);
        }

这样两次 O(n)的 for 循环就能得到maxLeft[] , maxRight[]

如此 ,我们就可以 O(1) 的获取当前位置 i 左边最高 maxLeft[i] , maxRight[i]

在结合刚刚的推倒式 : ans[i] =min(maxLeft[i] ,maxRight[i])-height[i] , 当前水柱高度 ans[i] 即可手撕

在累加求和可得所有水柱子的面积。

这个左右遍历预处理的思想很重要 , 可以使得我们 O(1)的获取自变量的值 , 我们直接带着这个思想看看下一题 ! (该题 js 和 python 代码 ,文末取 !)

135. 分发糖果

leetcode.cn/problems/ca…

老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。

你需要按照以下要求,帮助老师给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 相邻的孩子中,评分高的孩子必须获得更多的糖果。

那么这样下来,老师至少需要准备多少颗糖果呢?

示例 1:

  • 输入: [1,0,2]
  • 输出: 5
  • 解释: 你可以分别给这三个孩子分发 2、1、2 颗糖果。

示例 2:

  • 输入: [1,2,2]
  • 输出: 4
  • 解释: 你可以分别给这三个孩子分发 1、2、1 颗糖果。第三个孩子只得到 1 颗糖果,这已满足上述两个条件。

该题目思路其实和接雨水题目"左右遍历预处理出左右数组结果集, 之后对左右数组同一位置的值取最值"思想一致

我们理一下题目到底在云了什么 ?

我认为重点读懂这两个要求就行

  • 每个孩子至少分配到 1 个糖果。
    • 那就先给每个孩子们分配1个吧 , 之后比较的过程中有些1要修改,有些不需要修改 , 某种程度上减少了操作次数
  • 相邻的孩子中,评分高的孩子必须获得更多的糖果。
    • 所谓相邻 , 即左相邻 | 右相邻 , 所以就要分两种情况讨论 , 这就和接雨水有点类似了 ,下面我们具体来讲讲 。

我们可以分为左规则和右规则来讨论

我们把站成直线的孩子们抽象为数组ratings[] , 位置抽象为一个数组下标 i,表现度抽象为孩子高度(为了好说,抽象为高度) ratings[i], 现在选取一个位置i

左规则

分析左相邻 : i有可能比他左边高 , 由于要求用最少的糖果 ,那我就只在这个i对应的值上加1 。

右规则

分析右相邻 : i有可能比他右边高 , 由于要求用最少的糖果 ,那我就只在这个i对应的值上加1 。

由于题目要求的是要比相邻的孩子高, 糖果就要多,为了实现这个"多" ,我们只需要在相邻的高度上加1就实现了"多" , 那么到底在左相邻加1 还是 右加1 ?

其实最好的解法就是都加1 , 之后取左相邻 和 右相邻最大值就行 !

这个思路不就是接雨水的"左右遍历预处理出左右数组结果集, 之后对左右数组同一位置的值取最值"

(倔友可以结合文末代码思考 ,真的很有趣 !)

思路如下

  1. 必须满足左相邻规则和右相邻规则
  2. 从左向右遍历,获取left[]数组,均满足左规则
  3. 从右向左遍历,获取right[] 数组,均满足右规则
  4. 要想同时满足左右规则,必须取left[]和right[]数组每个位置对应的最大值

左规则

        // 从左到右遍历,更新 left 数组
        for (int i = 1; i < n; i++) {
            //比左边大就要在左边的基础上+1
            if (ratings[i] > ratings[i - 1]) {
                left[i] = left[i - 1] + 1;
            }
        }

右规则

 int count = left[n - 1];
        // 从右到左遍历,更新 right 数组并计算总数
        for (int i = n - 2; i >= 0; i--) {
            比右边大就要在右边的基础上+1
            if (ratings[i] > ratings[i + 1]) {
                right[i] = right[i + 1] + 1;
            }
        }

累加求和(可以在右规则时进行)

 for (int i = n - 2; i >= 0; i--) {
            if (ratings[i] > ratings[i + 1]) {
                right[i] = right[i + 1] + 1;
            }
            count += Math.max(left[i], right[i]);
        }    

代码附录

接雨水

python代码

    class Solution:
        def trap(self, height: List[int]) -> int:
            n = len(height)

            # 从左到右,遍历数组,寻找height[0]到height[i]的最大值pre_max[i]
            pre_max=[0]*n
            pre_max[0]=height[0]
            for i  in range(1,n):
                pre_max[i]=max(pre_max[i-1],height[i])
            # 从右到左,遍历数组 ,寻找height[i]到height[n-1]的最大值suf_max[i]
            suf_max=[0]*n
            suf_max[-1]=height[-1]
            for i in range(n-2,-1,-1):
                suf_max[i]=max(suf_max[i+1],height[i])

                
            ans=0
            # 对i位置左边的高度max和右边的高度max取最大值
            for h, pre ,suf in zip(height , pre_max ,suf_max):
                ans+=min(pre,suf)-h

            return ans






javaScript代码

var trap = function(height) {
    if (height.length === 0) {
        return 0;
    }

    let leftMax = [];
    let rightMax = [];

    // 从左到右遍历,计算每个位置左边的最大高度
    leftMax[0] = height[0];
    for (let i = 1; i < height.length; i++) {
        leftMax[i] = Math.max(leftMax[i - 1], height[i]);
    }

    // 从右到左遍历,计算每个位置右边的最大高度
    rightMax[height.length - 1] = height[height.length - 1];
    for (let i = height.length - 2; i >= 0; i--) {
        rightMax[i] = Math.max(rightMax[i + 1], height[i]);
    }

    let trappedWater = 0;

    // 遍历每个位置,计算该位置能接住的雨水量并累加
    for (let i = 0; i < height.length; i++) {
        trappedWater += Math.min(leftMax[i], rightMax[i]) - height[i];
    }

    return trappedWater;
};

分发糖果

python代码


       def candy(self, ratings) -> int:
        n = len(ratings)
        if n == 0: return 0
        left_to_right = [1] * n
        right_to_left = [1] * n
        # 找从左到右满足条件的
        for i in range(1, n):
            if ratings[i] > ratings[i - 1]:
                # 保证从左到右的最少个数
                left_to_right[i] = left_to_right[i - 1] + 1
        # print(left_to_right)
        # 找从右到左满足条件的(同时要符合从左到右)
        for i in range(n - 2, -1, -1):
            if ratings[i] > ratings[i + 1]:
                # 保证从左到右也满足, 同时也满足从右到左
                right_to_left[i] = max(left_to_right[i], right_to_left[i + 1] + 1)
        # print(right_to_left)
        res = 0
        # 选这个位置最大值
        for i in range(n):
            res += max(left_to_right[i], right_to_left[i])
        return res

javaScript代码

/**
 * @param {number[]} ratings
 * @return {number}
 */
var candy = function(ratings) {
    let n = ratings.length;
    if (n === 0) {
        return 0;
    }

    let leftToRight = new Array(n).fill(1);
    let rightToLeft = new Array(n).fill(1);

    // 找从左到右满足条件的
    for (let i = 1; i < n; i++) {
        if (ratings[i] > ratings[i - 1]) {
            // 保证从左到右的最少个数
            leftToRight[i] = leftToRight[i - 1] + 1;
        }
    }

    // 找从右到左满足条件的(同时要符合从左到右)
    for (let i = n - 2; i >= 0; i--) {
        if (ratings[i] > ratings[i + 1]) {
            // 保证从左到右也满足, 同时也满足从右到左
            rightToLeft[i] = Math.max(leftToRight[i], rightToLeft[i + 1] + 1);
        }
    }

    let res = 0;
    // 选这个位置最大值
    for (let i = 0; i < n; i++) {
        res += Math.max(leftToRight[i], rightToLeft[i]);
    }

    return res;
};

都看到这了 , 还不点个赞 ? ❤️