青训营 X 码上掘金 | 攒青豆中的动态规划

40 阅读2分钟

当青训营遇上码上掘金

在本次活动中,我选择主题4 攒青豆 作为我参与”码上掘金“活动的代码作品,这是一道算法题目。

题目说明

  • 现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)

攒青豆.png

以下为上图例子的解析:

输入:height = [5,0,2,1,4,0,1,0,3]
输出:17
解析:上面是由数组 [5,0,2,1,4,0,1,0,3] 表示的柱子高度,在这种情况下,可以接 17 个单位的青豆。

解题思路

如何定义接到青豆?

如果在当前柱子这一列,两旁存在比当前行高度高的柱子(注意:不一定是相邻行,这一行就可以存在接住的豆子。

所以整体思路是:遍历高度数组height,对于每一个高度height[i],寻找两旁最高的柱子中相对矮的一个 lower := min(max_left_height, max_right_height),因为对于两个高柱子盛豆子,取决于两个中更短的那个;当寻找到lower大于当前高度height[i],此时当前高度柱子可以留住豆子,留住豆子数量为lower - height[i];当lower高度小于当前高度,说明此时当前柱子上留住豆子数量为0;最后将每次留住豆子数量相加为最终结果。

考虑边界条件:遍历柱子不需要开头和结尾两个柱子,因为两个柱子边没有柱子,不可能存留豆子。

解题代码为

func trap(height []int) int {
    sum := 0

    for i := 1; i < len(height) - 1; i++ {
        max_left := getMax(height[:i])
        max_right := getMax(height[i+1:])
        lower := min(max_left, max_right)
        if lower > height[i] {
            sum += (lower - height[i])
        }
    }

    return sum
}

func getMax(nums []int) int {
    max := 0
    for _, v := range nums {
        if v >= max {
            max = v
        }
    }
    return max
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

时间复杂度为O(n^2),遍历数组长度为n,每次遍历到数组元素需左右寻找最高高度为n,结果为n*n。

优化思路

可以看到每遍历一个高度,就需要寻找两边最高柱子,这中间存在效率浪费,如果可以利用动态规划思路,记录每个位置上左右最大的高度柱子,可以减少遍历次数。

首先可以使用两个数组,max_right, max_left,表示对于height[i],左边最高柱子高度为max_left[i],右边最高高度为max_right[i]

对于 max_left我们其实可以这样求。

max_left [i] = Max(max_left [i-1],height[i-1])。它前边的墙的左边的最高高度和它前边的墙的高度选一个较大的,就是当前列左边最高的墙了。注意:观察迭代方程,当前元素i需要之前的元素i-1进行更新,所以遍历方向是从左到右

对于 max_right我们可以这样求。

max_right[i] = Max(max_right[i+1],height[i+1]) 。它后边的墙的右边的最高高度和它后边的墙的高度选一个较大的,就是当前列右边最高的墙了。注意:观察迭代方程,当前元素i需要之前的元素i+1进行更新,所以遍历方向是从右到左

如此调整过的代码为

func trap(height []int) int {
    sum := 0
    n := len(height)

    max_left := make([]int, n)
    max_right := make([]int, n)

    for i := 1; i < n - 1; i++ {
        max_left[i] = max(max_left[i - 1], height[i - 1])
    }
    for i := n - 2; i >= 0; i-- {
        max_right[i] = max(max_right[i + 1], height[i + 1])
    }

    for i := 1; i < len(height) - 1; i++ {
        lower := min(max_left[i], max_right[i])
        if lower > height[i] {
            sum += (lower - height[i])
        }
    }

    return sum
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

经过优化,此时时间复杂为O(n)。