当青训营遇上码上掘金之攒青豆

57 阅读3分钟

当青训营遇上码上掘金

题目: 攒青豆

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

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

这是一道经典的题目,与Leetcode上42. 接雨水 - 力扣(LeetCode)一致。

题解

动态规划

思考一个问题:要接住青豆,应该满足什么条件?生活经验告诉我们:要接住青豆,应该同时保证青豆不会从右边流走也不会从左边流走。 首先,要保证青豆不会从左边流走,我们需要保证所有当前列的青豆高度(加上柱子本身的高度)小于等于左边柱子的最大高度。同理,要保证青豆不会从右边流走,我们需要保证所有当前列的青豆高度(加上柱子本身的高度)小于等于右边柱子的最大高度。这样我们就能得到两个方向的最大高度,最后计算每一列青豆的高度(不包含当前列柱子高度)只需选两最大值中较小的值与柱子高度做差即可。

我们使用leftMax和rightMax两个数组表示两个方向下青豆加上柱子到达的最大高度。明显有leftMax[0]=height[0],rightMax[n-1]=height[n-1]。接着要计算leftMax数组,我们从左往右遍历,下标i处的最大高度应该为leftMax[i-1]与height[i]的较大者。同理,要计算rightMax,我们只需要从右往左遍历,下标j处的最大高度应该为rightMax[j+1]与height[j]的较大者。

  • 1i<n1\le i \lt n 时,有leftMax[i]=max(leftMax[i−1],height[i]);
  • n2j0n-2\ge j \ge 0 时,有rightMax[i]=max(rightMax[i+1],height[i])

下面用golang实现:

func trap(height []int) (ans int) {
    n := len(height)
    if n == 0 {
        return
    }

    leftMax := make([]int, n)
    leftMax[0] = height[0]
    for i := 1; i < n; i++ {
        leftMax[i] = max(leftMax[i-1], height[i])
    }

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

    for i, h := range height {
        ans += min(leftMax[i], rightMax[i]) - h
    }
    return
}

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

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

复杂度分析:

  • 时间复杂度:O(n)。其中n为height数组的长度。计算数组leftMax 和 rightMax,leftMax和rightMax的元素值各需要遍历 一次,计算能接的雨水总量还需要遍历一次。
  • 空间复杂度:O(n)。其中n为height数组的长度。算法需要额外使用leftMax和rightMax两个数组空间。

双指针

注意到下标 i 处能接的雨水量由leftMax[i]和rightMax[i]中的最小值决定。我们可以用left和right指针表示当前可以确定最大高度的位置。leftMax和rightMax表示当前已遍历的左边区域和右边区域的最大高度。有这么两种情况:

  • 如果leftMax < rightMax ,则left指针的位置的最终最大高度可以确定,即为当前的leftMax,所以有当前位置left能攒的青豆数为v=leftMaxheight[left]v=leftMax-height[left]
  • 如果leftMax >= rightMax ,则right指针的位置的最终最大高度可以确定,即为当前的rightMax,所以有当前位置right能攒的青豆数为v=rightMaxheight[right]v=rightMax-height[right]

下面用golang实现:

func trap(height []int) (ans int) {
    left, right := 0, len(height)-1
    leftMax, rightMax := 0, 0
    for left < right {
        leftMax = max(leftMax, height[left])
        rightMax = max(rightMax, height[right])
        if height[left] < height[right] {
            ans += leftMax - height[left]
            left++
        } else {
            ans += rightMax - height[right]
            right--
        }
    }
    return
}

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

复杂度分析:

  • 时间复杂度:O(n)。其中n为height数组的长度。只需要遍历一次height数组。
  • 空间复杂度:O(1)。只使用了两个指针和两个记录值。