攒青豆,通俗易懂的解法以及O(1)空间的优化

149 阅读3分钟

当青训营遇上码上掘金

问题描述

“攒青豆”和LeetCode的(42. 接雨水 - 力扣(LeetCode))类似。

有 n 个宽度为 1 的柱子,给定 n 个非负整数依次表示柱子的高度。

当青豆的高度超过旁边柱子高度时,会发生溢出,求在给定条件下能攒下的最多青豆,一个 1 乘 1 的格子代表一单位青豆,实际上是求能攒下的青豆面积。

问题分析

条件分析

青豆会“溢出”,代表一个柱子上的青豆高度取决于这个柱子左边最高的柱子和右边最高的柱子,这两根柱子的最小者。

解决思路

使用两个数组分别存储某个柱子左边柱子高度的最大值和右边柱子高度的最大值。

设第 i 根柱子高度为 height[i],第一个数组叫 lmax,lmax[i]的值表示第i根柱子左边最高柱子的高度,第二个数组叫 rmax,rmax[i]的值表示第 i 根柱子右边最高柱子的高度。

可以通过从左往右遍历得到 lmax,从右往左遍历得到 rmax。

for (int i = 1; i < len; i++)
{
    lmax[i] = max(lmax[i - 1], height[i]);
}
for (int i = len - 2; i >= 0; i--)
{
    rmax[i] = max(rmax[i + 1], height[i]);
}

得到左右两边最大值后,第i根柱子上的青豆高度就等于

min(lmax[i], rmax[i]) - height[i];

i 从 0 到 n-1 依次遍历,再把青豆高度累加即可得到答案。

代码和实现细节

完整代码放在码上掘金([攒青豆 - 码上掘金 (juejin.cn)(code.juejin.cn/pen/7194414…))

使用C++编写,里面的 getQingDou_old 函数就是这个算法的实现。

有人可能会发现,当第 i 根柱子很高时,它上面可能没有青豆,但代码中似乎并没有对这种情况进行判断,实际上,当第 i 根柱子的高度大于等于 min(lmax[i], rmax[i]) 时,这根柱子上就没有青豆,我们在生成 lmax 与 rmax 时,就考虑到了这种情况,因为当第 i 根柱子大于等于左边所有柱子高度时,lmax[i] 就等于 height[i],rmax同理。此时 min(lmax[i], rmax[i]) 一定等于 height[i],相减时就得 0 ,表示这根柱子上没有青豆。

复杂度分析

两次遍历生成 lmax 和 rmax ,还有一次遍历得到结果,时间复杂度为 O(n)

使用了两个长度为 n 的额外数组,空间复杂度为 O(n) 。

再优化实现O(1)空间

实现方法

我们不使用两个数组存储左右两边的最大值,而是使用两个变量 l_max 和 r_max 来存储左右两边的最大值。

同时,定义两个指针l,r 在开始时分别等于 0 和 n-1 ,l只向右边移动,r只向左边移动,移动时计算柱子上雨水的高度和更新 l_max 和 r_max 的值。

具体做法

初始时,l = 0,r = n-1,l_max = height[0],r_max = height[n-1]

设 l_height = height[l] ,r_height = height[r] ,当 l 和 r 没有相遇时,比较 l_height 与 r_height :

当 l_height < r_height 时,l_max 一定小于 r_max ,而 r_max 只是表示 r 及其右边的最大高度,说明 l_max 比 l 右边最大高度要低,因此 l 处的青豆高度根据前面的论述取决于 l_max ,等于 l_max - l_height ,让 l 增加,即向右移动,更新 l_max 为 max(l_max, height[l]) 。

当 l_height >= r_height 时同理,只不过此时计算的是 r 处的青豆高度,同时 r 向左移动,同时更新 r 与 r_max 。

当 l 与 r 相遇时即计算完了所有柱子上的青豆高度,得到结果。

部分代码如下:

while (l < r)
{
    int l_height = height[l];
    int r_height = height[r];
    if (l_height < r_height)
    {
        res += l_max - l_height;
        l++;
        l_max = max(l_max, height[l]);
    }
    else
    {
        res += r_max - r_height;
        r--;
        r_max = max(r_max, height[r]);
    }
}

完整代码也在码上掘金中,函数名为 getQingDou

复杂度分析

对 n 个元素遍历了一次,时间复杂度为 O(n)

使用了常数个变量,空间复杂度为 O(1)

总结

我们从最容易理解的动态规划算法出发,解决了这个问题,然后使用双指针实现了不使用额外数组空间的做法。

实际上这道题还有单调栈等做法,可谓有趣,也是一道经典算法题了。