用C++暴力攒青豆

84 阅读8分钟

当青训营遇上码上掘金,便开启了不一样的专栏创作。很高兴参加本次青训营,在本次青训营之中我们有个很调皮的嘉宾,那就是咱的青豆,为了能够治治青豆调皮的个性,我可是废了九牛二虎之力来抓住它,哈哈~为大家分享一下我如何用C++暴力攒青豆的!

✍️题目介绍

下面咱们来看一下原来的这个题目是怎样的,并分析在这个题目之中可能存在的情况,从特殊到一般情况,循序渐进的得出最终解答本题的方案。

👀题目原题

原题的描述如下:

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

image-20230113205519375.png

以下为上图例子的解析:

输入:height = [5,0,2,1,4,0,1,0,3]

输出:17

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

🤔题目分析

一开始拿到这道题时,便让我想起自己小时候玩米的经历。家里是农村的,小时候当父亲用打米机去去除谷子的壳,再把白花花的大米装进竹篓中时,每次我总是会用自己为数不多的积木,拼出一些奇形怪状的容器去盛米粒。刚出机器的大米热乎乎的,装进自己的容器里感觉很好玩,但是由于积木的不足,很多米粒总是会从最矮的积木漏出。

受到这个生活经验的启发,可以知道这个题中青豆的单位数量,是受到相邻最短的那根柱子影响的。但是从最短的柱子入手的话貌似不好想出应该怎么解决,而且本题和所述情况还有点大不一样,因为它是一维的,而且顺着想确实想了很久也没有方案,为此咱们就反着想(数学中这叫【反证法】),从最高的那根柱子入手,这个时候其实会有三种大的情况:

  1. 当最高的柱子在最左边的时候
  • 当最高的柱子在最左边,右边的柱子全部为最依次递减的情况(特殊情况1)

    这种情况模拟数组为[5,0,3,0,2],如下图所示:

图稿1.jpg

在这个图之中我们可以发现,当最高的柱子在最左边,右边的柱子呈现出依次依次递减的情况下,能攒的单位青豆数取决于相邻的右边柱子。例如图中a柱子与b柱子的高度差为2,两个柱子之间能够容纳的单位青豆数为3,又如b柱子和c柱子之间高度相差1,这个时候两个柱子之间能够容纳的青豆数为2,最终的结果为3+2=5个单位青豆。

  • 当最高的柱子在最左边,最右边的柱子总是大于等于最左边的柱子的情况(特殊情况2)

这种情况模拟数组为[5,0,2,0,3],如下图所示:

图搞2.jpg

在这个图中我们可以发现,它不再像【当最高的柱子在最左边,右边的柱子全部为最依次递减的情况(青豆数目取决于相邻最右边的柱子情况)】,而是取决于最右边的柱子。例如图中的a柱子和c柱子高度相差2,有效高度为3,然后a柱子~c柱子之间间隔了3个柱子的距离,若之间不存在柱子,则应该有的单位青豆数量为【有效柱子高度*柱子间距】这里也就是3*3=9个单位青豆,推广来看,中间若存在柱子的话,则上面计算的基础上减去中间所有柱子的高度即可获得最终的单位青豆数目,所以最终的单位青豆为【有效柱子高度*柱子间距-中间所有柱子的高度合】,这里也就是3*3-2=7个单位青豆。

  • 当最高的柱子在最左边,右边的柱子参差不齐的时候(一般情况1)

    这种情况模拟数组为[5,0,2,0,3,1,2],如下图所示:

图稿3.jpg

在这个图中我们可以发现,这种情况实际上是上述两种情况的综合,但是进一步分析可以得出,实际上可以看成从最右边开始,依次往左边靠拢时,右边相对于左边临近柱子长度要大的右边柱子的平移,减去比其矮的柱子高度即可求出单位青豆。

  1. 当最高的柱子在最右边的时候
  • 当最高的柱子在最右边的时候,左边的柱子从右到左全部依次递减的情况(特殊情况3)

    这种情况模拟数组为[2,0,3,0,4],如下图所示:

图稿4.jpg

通过这个图中我们可以发现,单位青豆的数目应该为相邻柱子的高度差。这于当最高的柱子在最左边是一样的道理,只是反过来了而已。

  • 当最高的柱子在最右边,左边的柱子从右到左依次递增的时候(特殊情况4)

    这种情况模拟数组[3,0,2,1,0,4],如下图所示:

图搞5.jpg

通过这个图中我们可以发现,它不再像【当最高的柱子在最右边的时候,左边的柱子从右到左全部依次递减的情况(青豆数目取决于相邻最左边的柱子情况)】,而是取决于最左边的柱子。例如图中的a柱子和d柱子高度相差1,有效高度为3,然后a柱子~d柱子之间间隔了4个柱子的距离,若之间不存在柱子,则应该有的单位青豆数量为【有效柱子高度*柱子间距】这里也就是3*3=9个单位青豆,推广来看,中间若存在柱子的话,则上面计算的基础上减去中间所有柱子的高度即可获得最终的单位青豆数目,所以最终的单位青豆为【有效柱子高度*柱子间距-中间所有柱子的高度合】,这里也就是3*3-2=7个单位青豆。

  • 当最高的柱子在最右边,左边的柱子参差不齐的时候(一般情况2)

这种情况模拟数组为[2,3,0,2,1,0,4],如下图所示:

图搞6.jpg

通过这个图中我们可以发现,实际上我们可以从最左边开始固定最右边,移动最左边,实现滑动窗口,来求解。例如找到从最左边开始,次要较大的,并将次要较大的向右边移动,用以在找到比它还高的柱子之前再乘上他的距离或者直接每移动一次就加上左边开始的相比而言高的柱子,最后减去移动时中间的比移动柱子自身要矮的柱子数目既是最终的单位青豆数目。

  1. 当最高的柱子在中间的时候

这种情况如下图所示:

图搞7.jpg

当最高柱子在中间的时候其实是上述两种一般情况的综合,如图所示,首要要做的就是在不排序的情况下找到最高的那根柱子,然后以最高柱子为中心可以将其分为两种个区域一个是left area一个是right area,那么紧接着就是按照上面的操作步骤分别对left area部分进行单位青豆的数量求解以及对right area部分进行单位青豆的数量求解,最后求和即可得出最终答案。

🥳题目解决代码

通过上述情况的综合分析,我们可以在最后考虑第3种大的情况进行求解,因为第1和第2种大的情况无非是第3种大的情况的特殊情况,即left area不存在柱子或者right area不存在柱子。由此可以得出下述代码:

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

class Solution {
public:
    static int trap(vector<int>& height) {
        int max_index{ int(max_element(height.begin(),height.end()) - height.begin()) };// 找到最高的那根柱子的下标
        int right_max{ int(height.size() - 1) };//右边最大小标
        int left_max{};//左边最大下标

        /*处理左边数据*/
        int left_re{};// 用于记录右边的数据结果
      	/*右端固定的滑动窗口*/
        for (int index_move = left_max + 1; index_move < max_index; index_move++) {
            if (height[left_max] <= height[index_move]) {
                left_max = index_move;
                continue;
            }
            left_re += (height[left_max] - height[index_move]);
        }

        /*处理右边数据*/
        int right_re{};// 用于记录左边数据的最终单位青豆
        int right_re_temp{};// 用于记录次高中间数据的柱子高度
      	/*左端固定的滑动窗口*/
        for (int index_move = right_max; index_move > max_index; index_move--) {
          /*如果遇到左边的柱子比右边的柱子要高或等于的情况*/
            if (height[right_max] <= height[index_move]) {
                right_re_temp += height[index_move];
                right_max = index_move;
                right_re += height[right_max];
                continue;
            }
          /*如果遇到左边柱子比右边柱子要矮的情况*/
            right_re_temp += height[index_move];
            right_re += height[right_max];
        }
      	/*所有次要高柱子的平移和减去所有比次要高柱子矮的柱子的和*/
        right_re = right_re -  right_re_temp;

        return right_re + left_re;
    }
};

int main(int argc, char* argv[]) {
    /*输入数据*/
    ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
    vector<int>data;
    int num_temp{};
    while (cin >> num_temp) {
        data.push_back(num_temp);
    }

    /*处理数据,并输出最终结果*/
    cout << Solution::trap(data);

    return 0;
}

😊代码执行情况

代码的正确与否需要验证,可以在下面这里进行值的输入,用来进行代码的校验。

💁题目总结

这道题最优的方法可能不是这种,在此可以总结出做题的一般套路。

对于一道程序题,它主要的三个大的板块是:

【输入相关数据并存储->处理数据->按要求输出处理结果。】

在我看来所谓的算法实际上是将自己所想的,实现在处理数据这一板块上,例如在上述代码之中,我将这三个部分进行了分离,首先是对数据的输入进行存储,然后将解决问题的方法进行封装,并调用所封装的方法解决问题并输出结果。 调皮的青豆就这样被驯服了,哈哈~😉