当青训营遇上码上掘金
今天简单做了一下这次青训营的主题创作,选择的主题是后端学员推荐的主题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;
}