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

79 阅读3分钟

当青训营遇上码上掘金

本次青训营,主办方为了鼓励我们积极学习参与项目制作,推出一套积分制度也就是“攒青豆”,达到一定青豆数量的学院可以获得奖励和证书~那么这道题就是以此次活动为背景的一道题目。

话不多说我将会从刷题的视角一步一步解决问题。

问题描述:

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

image.png 看图能知道要想拿到豆子就需要一个“凹”型,观察发现对于每一列所能拿到的豆子数是当前列的宽乘上当前列左右两边最高列中取最小值,比如能取到豆子的第一列,他的左边最高柱子是5,右边最高柱子为4,那么他能取到的豆子就是1*4=4,往后依次是这个规律

那么我们就能写出如下的代码:

image.png 这段代码在码上掘金是可以跑通的,提一嘴这个插件用起来确实不错。 不过这样的时间复杂度是On2,因为要遍历每一列两边的最高柱子,相当于每走一次就要向两边去探测

有没有更快的方法呢?

降低时间复杂度的方法最简单就是空间换时间,很容易想到,既然每次都要遍历一下柱子求左右两边的最高柱子,那么我们可以定义两个数组去遍历保存一下每一个柱子左右两边的最高柱子的高度,也可以利用对组去一次把左右两边的都存起来,我为了方便下面代码就用两个数组写了

代码示例:也是可以跑通的

int trap(vector<int>& height) {
       if(height.size()<=2)
        {
            return 0;
        }
        //记录每个柱子左边最高的柱子高度
        vector<int>ml(height.size());
        //同理右边
        vector<int>mr(height.size());
        int n=height.size();
 
        ml[0]=height[0];
        mr[n-1]=height[n-1];
        //存到备忘录里
        for(int i=1;i<n;++i){
            ml[i]=max(height[i],ml[i-1]);
        }
        for(int i=n-2;i>=0;--i)
        {
            mr[i]=max(height[i],mr[i+1]);
        }
 
        int sum=0;
        for(int i=0;i<n;++i){
            int tmp=min(ml[i],mr[i])-height[i];
            if(tmp>0){
                 sum+=tmp;
            }    
        }
        return sum;
    }
那么还能不能节省空间利用O1的空间,On的时间呢?

答案是有的:

我们利用双指针在一次遍历计算的同时去更新保存左右最高的柱子,代替了两个数组。不过双指针的如何移动呢?巧妙之处就在于我们虽然需要左右两个最高的柱子,但其实在计算存储豆子的数量的时候,我们只需要知道两个最高的里面最矮的那个就可以了,能get到吧

先看代码再解释:

int trap(vector<int>& height) {
        int res=0;
        int left=0,right=height.size()-1;
        int n = height.size();
        int l_max = height[0];
        int r_max = height[n - 1];
        while(left<right)//每次更新左右高度的最大值
        {
            l_max = max(l_max, height[left]);
             r_max = max(r_max, height[right]);
        
        // ans += min(l_max, r_max) - height[i]
        if (l_max < r_max) {
            res += l_max - height[left];
            left++; 
        } else {
            res += r_max - height[right];
            right--;
        }
 
        }
        return res;
    }
  • 1、定义两个指针 (左、右指针)
  • 2、定义左、右的边界最大值,并在移动中更新这两个max(用于计算比他低的差值)。
  • 3、比较2个指针所在位置的高度,然后谁小谁先移动(左++,右-- 两边收紧模式)
  • 4、移动的同时计算差值(谁小谁先动,另一边一定比他高所以不用担心漏的问题)
  • ps:左右边界在移动中 ,高度都取max,所以在递增情况时,max即当前值,total便不会增加。

假设一开始lmax大于rmax,则之后right会一直向左移动,直到rmax大于lmax。在这段时间内right所遍历的所有点都是左侧最高点lmax大于右侧最高点rmax的,所以只需要根据原则判断rmax与当前高度的关系就行。反之left右移,所经过的点只要判断lmax与当前高度的关系就行。

整体思路很类似于快排的那种挖坑,从两边往中间缩进

当然了肯定很多人都能想到的单调栈的思路去解决这个问题,我大概说一下

单调栈一般解决的问题都是类似的,找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,用空间去换时间的一种思路,栈里一般放的是元素下标

这道题的话就用单调递减的栈,如果当后面的柱子高度比前面的低时,是无法存储的,如果后面柱子比当前柱子高,那么才能存青豆,遇到了计算能拿多少个青豆,再出栈看之前栈里的柱子能还能不能拿所以就是两个循环

当遇到了比栈顶高的柱子了,就需要计算栈顶柱子能存多少豆子了,遍历到的柱子就是右边界,栈顶pop之后的栈顶就是左边界,二者取较小的去计算存储豆子。

看代码:

int trap(vector<int>& height) {
        stack<int>sk;
        sk.push(0);
        int sum=0;
        //单调栈其实是一层一层存豆子
        for(int i=1;i<height.size();++i)
        {
            while(!sk.empty()&&height[i]>height[sk.top()])
            {
                int cur=sk.top();//当前要判断的
                sk.pop();
                if(!sk.empty())
                { 
                int left=sk.top();//最左边
                int tmp=min(height[left],height[i])-height[cur];
                sum+=(i-left-1)*tmp;
                }
            }
            sk.push(i);
        }
        return sum;
      

    }