前言
夜已深 , 耳机里萦绕着 程响的歌曲《在他乡》, 我贸然敲下这篇文章 , 敬各位深夜孤勇者 ~
看了很多算法博客 , 很多都是在搞一题多解 , 就是在深度上做文章 , 笔者算法小菜鸡 , 就不在这与大佬们百家争鸣了 。(后续还会在深度上做文章,经典的题目总是要多刷几遍的😀)
笔者将带来两道看似"老死不相往来"的题目 , 让他们联姻 !
也许有倔友以为 "横向归一" 是题目跳跃游戏和跳跃游戏 II 般"天造地设" , 其实不是 , 他们是"有缘千里来相会" , 所谓"金风玉露一相逢,便胜却人间无数" !
请各位倔友往下看
42. 接雨水
给定 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. 分发糖果
老师想给孩子们分发糖果,有 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 , 之后取左相邻 和 右相邻最大值就行 !
这个思路不就是接雨水的"左右遍历预处理出左右数组结果集, 之后对左右数组同一位置的值取最值"
(倔友可以结合文末代码思考 ,真的很有趣 !)
思路如下
- 必须满足左相邻规则和右相邻规则
- 从左向右遍历,获取left[]数组,均满足左规则
- 从右向左遍历,获取right[] 数组,均满足右规则
- 要想同时满足左右规则,必须取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;
};
都看到这了 , 还不点个赞 ? ❤️