「青训营 X 码上掘金」主题1:攒青豆

77 阅读2分钟

当青训营遇上码上掘金

今天简单做了一下这次青训营的主题创作,选择的主题是后端学员推荐的主题4:攒青豆。题目根据青训营情况换了个名字,但问题本质就是接雨水。

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

有一个简单的想法是把问题抽象到二维平面,类似于在每一个离散(从最低到最高)的高度值(竖轴)上进行从左到右的扫描。使用一个标记变量记录当前状态,每到一个位置(横轴)检查此处高度,若该位置的柱子高于当前高度,则标记变量发生翻转。每两次翻转之间的长度就是该高度值可以容纳的青豆面积。将所有高度扫描过后就可以算出总面积。但这种方法时间复杂度有O(N*H), H为最高的柱子高度,效率太低。

比较常规的做法是使用单调栈。首先将计算总面积的方法做一个变换:对于横轴的每一个柱子,求其左右比它高的边界柱子的高度两者的较小值,用这个值减去该柱子高度后,再乘左右边界柱子之间的距离。对每一个柱子作此计算求和后即为总面积。

单调栈是一个栈,栈中元素从栈底到栈顶按一定顺序(升序/降序)进行排列。我们使用递减栈做题,依次输入柱子高度,当输入柱子的高度小于栈顶元素时,说明该柱子的左边界为栈顶元素,将输入柱子正常压栈;而当输入柱子高度大于栈顶元素时,需要将栈中元素逐个抛出直到栈顶元素大于当前柱子高度,这些被抛出的元素(柱子)的右边界即为当前柱子,而当前柱子的左边界即为新的栈顶,然后将输入柱子入栈。通过单调栈能方便地得到某个柱子左右边界的信息。时间复杂度为O(N)。

代码如下:

public int GetBeans(int [] input)
  {
    int [] LeftHigher = new int [input.length];
    int [] RightHigher = new int [input.length];
    Stack<Integer> IncStack = new Stack<Integer>();

    for(int i = 0; i < input.length; i++)
    {
      if(IncStack.isEmpty() || input[i] < input[IncStack.peek()])
      {
        LeftHigher[i] = IncStack.isEmpty()? -1 : IncStack.peek();
        IncStack.push(i);
      }
      else
      {
        while(!IncStack.isEmpty() && input[IncStack.peek()] < input[i])
        {
          RightHigher[IncStack.pop()] = i;
        }
        LeftHigher[i] = IncStack.isEmpty()? -1 : IncStack.peek();
        IncStack.push(i);
      }
    }

    //clear remaining value in stack
    while(!IncStack.isEmpty())
    {
      RightHigher[IncStack.pop()] = input.length;
    }

    int sum = 0;
    for(int i = 0; i < input.length; i++)
    {
      //System.out.println("index: " + i + " L: " + LeftHigher[i] + " R: " + RightHigher[i]);
      if(LeftHigher[i] < 0 || RightHigher[i] >= input.length) continue; //too high, no storage formed
      int h = Math.min(input[RightHigher[i]], input[LeftHigher[i]]);
      sum += ( (h - input[i]) * (RightHigher[i] - LeftHigher[i] - 1));
    }
    return sum;
  }