「青训营 X 码上掘金」主题 4:攒一点青豆

当青训营遇上码上掘金

那么会发生什么神奇的化学刺激呢(锤锤笑(非锤粉))!?

image.png

我选择的主题是 4:攒青豆

主题 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:每行数

  直接数每一行。先遍历一次每个柱子,得到柱子的最大高度,然后逐行开始遍历,从第一行往上到最高的柱子层数逐行去数,只有当当前的柱子高度小于 i 并且两边有柱子高度大于等于 i,则能够收集到一个单位的青豆,注意在每遇到一个柱子的时候将每行攒到的青豆数进行更新,这样既可以数到每个区域的青豆,也能够防止边角堆积。根据这个规则按行从下往上遍历所有的柱子便可以得到 height 情况下能够攒到的所有青豆。
  代码如下:

#include<iostream>
#include<vector>
using namespace std;
int main() {
    vector<int> height = {5,0,2,1,4,0,1,0,3};
    int n = height.size();
    // 保存结果
    int res = 0;
    // 便利一遍找出最大值,用于行扫描
    int top = 0;
    for (int i = 0; i < n; ++i) {
        top = height[i] > top ? height[i] : top;
    }
    // 逐行扫描
    for (int i = 1; i <= top; ++i) {
        // 标记是否进行更新
        bool flag = false; 
        int temp = 0;
        for (int j = 0; j < n; ++j) {
            if (flag && height[j] < i) {
                ++temp;
            }
            if (height[j] >= i) {
                res = res + temp;
                temp = 0;
                flag = true;
            }
        }
    }
    cout << "本次情况可以攒到" << res << "颗青豆!" << endl;
    return 0;
}

时间复杂度:柱子数量为 n,最大柱子长度为 m,时间复杂度为 O(mn)。
空间复杂度:变量个数为常数,空间复杂度为 O(1)。

思路2:每列数(动态规划)

  对于每一个 height[i],得到 height[i] 左边的最大高度 lmax[i] 和 height[i] 右边的最大高度 rmax[i],用 height[i] 左右最大高度的最小值 min(lmax[i],rmax[i]) 减去 height[i] 的高度能够得到 i 列所能得到的单位青豆数,当左右高度最小值小于 height[i] 则无法攒到青豆,根据这个规则遍历所有位置的柱子便可以得到 height 情况下能够攒到的所有青豆。
  代码如下:

#include<iostream>
#include<vector>
using namespace std;
int main() {
    vector<int> height = {5,0,2,1,4,0,1,0,3};
     // 动态规划
    int n = height.size();
    if (n == 0) return 0;
    vector<int> lmax(n), rmax(n);
    lmax[0] = height[0];
    rmax[n - 1] = height[n - 1];
    for (int i = 1; i < n; ++i) {
        lmax[i] = max(lmax[i - 1], height[i]);
        rmax[n - 1 - i] = max(rmax[n - i], height[n - 1 - i]);
    }
    int res = 0;
    for (int i = 0; i < n; ++i) {
        res += min(lmax[i], rmax[i]) - height[i];
    }
    cout << "本次情况可以攒到" << res << "颗青豆!" << endl;
    return 0;
}

 时间复杂度分析:柱子数量 n,进行了两次(常数级)扫描,时间复杂度为 O(2n) -> O(n)。
 空间复杂度分析:创建了左右两个最大值的数组,空间复杂度为 O(2n) -> O(n)。

思路2优化:(双指针)

思路 2 中用到了两个数组 lmax 和 rmax,在使用 lmax 和 rmax 的过程中可以不用保存先前的数据,由于水桶的短边原理,这样便能够将两个数组换成两个变量,每次只对两者较短边的柱子进行青豆的收集,同时移动柱子较短边的指针,这样便能够收集到所有的青豆。
  代码如下:

#include<iostream>
#include<vector>
using namespace std;
int main() {
    vector<int> height = {5,0,2,1,4,0,1,0,3};
    int n = height.size();
    if(n == 0) return 0;
    int res = 0;
    int left = 0, right = height.size() - 1;
    int lmax = 0, rmax = 0;
    while(left < right){
        lmax = max(lmax, height[left]);
        rmax = max(rmax, height[right]);
        if(rmax > lmax)
            res += lmax - height[left++];
        else
            res += rmax - height[right--];
    }
    cout << "本次情况可以攒到" << res << "颗青豆!" << endl;
    return 0;
}

 时间复杂度分析:柱子数量 n,进行了一次扫描,时间复杂度为 O(n)。
 空间复杂度分析:创建了常数级的空间,空间复杂度为 O(1)。

这里的青豆攒好了!!!