「青训营 X 码上掘金」创作"主题4"——攒青豆

108 阅读6分钟

当青训营遇上码上掘金

前言

如题所示,本文「青训营 X 码上掘金」主题创作的选题为攒青豆!

「攒青豆」题目说明

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

官方给的题目样例图: image.png

以下为上图例子的解析:

inputheight = [5,0,2,1,4,0,1,0,3] 
output:17 

解析:上面是由数组 [5,0,2,1,4,0,1,0,3] 表示的柱子高度,最后能接17个单位豆子

题目解读

  1. 青豆和柱子的宽度一样,属于一个单位的宽度
  2. 拿第一个柱子举例,5个单位的青豆相当于一个高度为5的柱子,因此我们可以将一个青豆近似看作一个1x1的正方形
  3. 青豆可以看作绿色的正方形,而柱子可以看作是黑色正方形拼出
  4. 按照(2)和(3)的说法,而柱子算作相同形状的另一种黑子的正方形按照不同个数搭出的柱子
  5. 注意不考虑边角堆积的条件,即若前两个柱子为5,10,由于第一个柱子的左边是空的,虽然第二个柱子比第一个柱子高,但是仍然无法接到青豆,即需要形成杯子容器的攒青豆模型
  6. 题外,感觉有点像漏桶模型,需要看最矮的木头

思路与代码

本人较为熟悉c++,但是为了方便就使用python来进行算法书写(绝对不是懒QWQ )

1 最为推荐的方法——双指针:

  • 作为一个合格的后端,对于传入的height数组需要判断是否存在
  • 不存在就返回-1,后续其他方法也是这样处理
  • maxLptr和maxRptr初始分别指向左右两部分的最左边和最右边的柱子
  • 通过比较左右两端的高度,选取值较小的柱子,往中间数青豆,每次移动1一格
  • 选取容器中较小的柱子进行青豆计算是算法关键,如同漏桶一样,需要看最爱的木头才能知道能装多少水,
  • 需要二次条件判断,要保证两端(容器)柱子中较小的柱子,要比当前的柱子大,才能得到真正需要累加的青豆数,若当前柱子小于两端(容器)柱子中较小的柱子,就需要进行对最大左(右)柱子的高度的更新
  • 最后,sum的值在移动中进行累加,当l和r相等时,sum即为所求答案。
def pointWay(height):
    if not height:
        return -1
    l, r = 0, len(height) - 1
    sum = 0
    maxLptr = 0 # maxLptr代表从最左边开始左指针搜寻到的最高柱子
    maxRptr = 0 # maxRptr代表从最右开始边右指针搜寻到的最高柱子
    # 每次移动1一格,从两端往中间找,左右柱子比较,哪边小先算哪边的青豆,选择左边的原因是,就算中间有比较高的柱子,选最小的计算青豆是保证了算法它的正确性,同时保证了作的差值不会小于0
    while l < r:
        if height[l] < height[r]:
            # 左边柱子较小
            if height[l] == maxLptr:
                l = l + 1
                continue
            elif height[l] > maxLptr:
                maxLptr = height[l]
            else:
                sum += maxLptr - height[l] 
            l = l + 1
        else:
            # 右边柱子较小
            if height[r] == maxRptr:
                r = r - 1
                continue
            elif height[r] > maxRptr:
                maxRptr = height[r]
            else:
                sum += maxRptr - height[r]
            r = r - 1
    return sum

2 直接法和动态规划

2.1 直接(暴力)的解法的思路

  • 作为一个合格的后端,对于传入的height数组需要判断是否存在
  • 循环需要从第二个柱子开始计算
  • 对于每一个柱子,通过传入当前柱子的下标i,向左向右通过max函数找到左右两边比当前柱子i还高的柱子
  • height[n:m],左闭右开,不写默认找到0或者数组最后一个元素
  • 最后,累加计算出能够接住的青豆数量,注意,代码中要考虑边界问题,像我的代码需要考虑maxRight为当前柱子且值比maxLeft大的的情况,需要做一个条件判断
  • 弊端:时间花费较多,O(n^2)
  • 具体看码上掘金平台中directWay函数代码

2.2 动态规划dp的解法的思路

  • 初始化固定值:最左柱子的左边为当前柱子的最大高度,最右柱子的右边为当前柱子的高度,进行初始状态
  • 根据最左柱子高度值,从左往右进行状态转移,每个柱子的最左高度,左闭右开,不关心最左柱子
  • 根据最右柱子高度值,从右往左进行状态转移,每个柱子的最右高度,左闭右开,不关心最右柱子
  • 累加算法跟直接法一致
  • 弊端:空间花费较多,需要多构造二个相同大小数组记录进行状态转移
  • 具体看码上掘金平台中dpWay函数代码

3 用栈方法

  • 思路:填块

  • 数据结构:C++:stack Python:list仿制stack

  • 具体看码上掘金平台中stackWay代码

  • 条件:while s and height[i] > height[s[-1]]:,若满足栈非空且当前柱子高度大于上一个入栈的柱子高度,就进行填块循环中,否则,下标值入栈。

  • "填块循环":对于每一个柱子,将这个柱子与栈中的柱子进行比较,如果当前柱子比栈顶柱子高,则弹出栈顶元素并计算出被这两根柱子围成的区域能够接住的青豆数量

  • 经过上述过程,进行累加得到sum后即为最后攒到的青豆个数

  • 具体看码上掘金平台中StackWay函数代码

心得

经过攒青豆的算法书写,我的python水平当然有所提高,这也是对动态规划进行了重新的学习(学数据结构时候不太清楚),python中没stack数据结构,但是c++有,所以改写到python时候花了点时间,最后参考一些文章对栈算法进行书写,所以在python中只能通过list来模仿栈的数据结构(如果写类的话代码就太多了,而我只需要其中一些内容就行),最后还是推荐双指针,在空间和时间上都是O(N),就是思路有些难想,不过想通了就豁然开朗了,直接法和动态规划的算法其实都差不多,只不过一个是时间上花费多,一个是空间上花费多,栈算法关键思路在于“填块”,个人感觉更难想,不如双指针,还是希望大家能花点时间都仔细看看“攒青豆”算法。

具体代码:已经放在码上掘金平台,地址:GreenBeans - 码上掘金 (juejin.cn),其中也写了较为详细的注释

引用

来源:「青训营 X 码上掘金」主题创作活动入营版 开启! - 掘金 (juejin.cn)