当青训营遇上码上掘金|主题4攒青豆|青训营

37 阅读3分钟

当青训营遇上码上掘金

题目描述

主题 4:攒青豆

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

image.png

以下为上图例子的解析:

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

解题路线

这道题十分契合这次青训营的活动,可以说调动了大家开始努力攒青豆获得结营证书的决心。然而笔者经过过“艰难”解题,并搜索相关知识时,发现其实这道题就是传播度很广的接雨水问题,可以说是大厂面试经典题目。经过学习和整理,本文将会按顺序依次按照双指针、动态规划、单调栈的方式来解题。

双指针

可以看出每一列雨水的高度,取决于该列左侧最高的柱子和右侧最高的柱子中最矮的那个柱子的高度。其宽度为1,高度即为体积。只要从头遍历一遍所有的列,然后求出每一列雨水的体积,相加之后就是总雨水的体积了。

func trap(height []int) int {
	var left, right, leftMax, rightMax, res int
	right = len(height) - 1
	for left < right {
		if height[left] < height[right] {
			if height[left] >= leftMax {
				leftMax = height[left]  // 设置左边最高柱子
			} else {
				res += leftMax - height[left]  // //右边必定有柱子挡水,所以遇到所有值小于等于leftMax的,全部加入水池中
			}
			left++
		} else {
			if height[right] > rightMax { 
				rightMax = height[right]  // //设置右边最高柱子
			} else {
				res += rightMax - height[right]  // //左边必定有柱子挡水,所以,遇到所有值小于等于rightMax的,全部加入水池
			}
			right--
		}
	}
	return res
}

动态规划

在上面的方法中为了得到两边的最高高度,使用了双指针来遍历,每到一个柱子都向两边遍历一遍,这其实是有重复计算的。
我们把每一个位置的左边最高高度记录在一个数组上(maxLeft),右边最高高度记录在一个数组上(maxRight)。这样就避免了重复计算,这就用到了动态规划。

func trap(height []int) int {
    sum:=0
    n:=len(height)
    lh:=make([]int,n)
    rh:=make([]int,n)
    lh[0]=height[0]
    rh[n-1]=height[n-1]
    for i:=1;i<n;i++{
        lh[i]=max(lh[i-1],height[i])
    }
    for i:=n-2;i>=0;i--{
        rh[i]=max(rh[i+1],height[i])
    }
    for i:=1;i<n-1;i++{
        h:=min(rh[i],lh[i])-height[i]
        if h>0{
            sum+=h
        }
    }
    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
}

单调栈

从栈头(元素从栈头弹出)到栈底的顺序应该是从小到大的顺序。 因为一旦发现添加的柱子高度大于栈头元素了,此时就出现凹槽了,栈头元素就是凹槽底部的柱子,栈头第二个元素就是凹槽左边的柱子,而添加的元素就是凹槽右边的柱子。
单调栈,其实是通过 长 * 宽 来计算雨水面积的。 长就是通过柱子的高度来计算,宽是通过柱子之间的下标来计算。

func trap(height []int) int {
   if len(height) <= 2 {
      return 0
   }
   st := make([]int, 1, len(height)) // 切片模拟单调栈,st存储的是高度数组下标
   var res int
   for i := 1; i < len(height); i++ {
      if height[i] < height[st[len(st)-1]] {
         st = append(st, i)
      } else if height[i] == height[st[len(st)-1]] {
         st = st[:len(st)-1] // 比较的新元素和栈顶的元素相等,去掉栈中的,入栈新元素下标
         st = append(st, i)
      } else {
         for len(st) != 0 && height[i] > height[st[len(st)-1]] {
            top := st[len(st)-1]
            st = st[:len(st)-1]
            if len(st) != 0 {
               tmp := (min(height[i], height[st[len(st)-1]]) - height[top]) * (i - st[len(st)-1] - 1)
               res += tmp
            }
         }
         st = append(st, i)
      }
   }
   return res
}


func min(x, y int) int {
   if x >= y {
      return y
   }
   return x
}

笔者代码段

-- 写作不易,感谢理解。欢迎批评指正。