当青训营遇上码上掘金之”攒青豆“

40 阅读3分钟

当青训营遇上码上掘金

题目:攒青豆

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

image.png

解题思路

解法一:双指针

image.png
观察上图可以得到一般规律,在铺满青豆后,整个图形应以最高的柱子为界,其左边从左到右不严格递增,其右边从右到左不严格递增。
找到规律后可以考虑解法:一次遍历找到最高柱子的高度和位置,第二次遍历时先从左到右遍历到最高的柱子的位置,同时记录遍历过程中遇到的柱子最高值,如果此时某位置上的柱子矮于当前记录最高值,说明必须在该位置上填充青豆,填充青豆数为leftMaxHeightheight[i]:leftMaxHeight - height[i]

代码实现(C++)

int trapA(vector<int>& height) {
  int len = height.size();
  if (len <= 2)  return 0;//当数组长度小于等于2个时,此时没有凹陷区域可以攒青豆,直接返回0

  int maxHeight = 0;// 记录数组中最高的墙
  int maxIndex = 0;// 记录数组中最高墙的下标

  for (int i = 0;i < len;i++) {
    if (maxHeight < height[i]) {
      maxHeight = height[i];
      maxIndex = i;
    }
  }

  int res = 0;// 可攒青豆数

  int lmax = height[0];// 记录从左往右遍历过程中的当前最大值

  // 从左往右遍历至最高点处,此时在攒满青豆后,整体应呈现从左往右不严格单调增的趋势,因此在遍历过程中,若遇到不满足的情况则补充青豆
  for (int i = 1;i < maxIndex;i++){
    if (height[i] < lmax){
      // 用青豆将数组填补成不严格单调增
      res += lmax - height[i];
    }else{
      lmax = height[i];
    }
  }

  int rmax = height[len - 1];// 记录从右往左遍历过程中的当前最大值

  // 从右往左遍历至最高点处,此时在攒满青豆后,整体应呈现从右往左不严格单调增的趋势,因此在遍历过程中,若遇到不满足的情况则补充青豆
  for (int j = len - 2;j > maxIndex;j--){
    if (height[j] < rmax){
      // 用青豆将数组填补成不严格单调增
      res += rmax - height[j];
    }else{
      rmax = height[j];
    }
  }

  return res;
}

时间复杂度:O(n)

空间复杂度:O(1)

解法二:单调栈

用单调栈找每个柱子左边第一个比它高的位置,累加两柱子之间的可攒青豆量,注意最后栈内元素是呈降序排列的。 模拟过程如下:
1.前面的柱子已经按照降序排列,现插入高度为3的柱子

2.计算与栈顶柱子之间的可攒青豆数,preHeight存储的是上一次出栈的柱子高度,则可攒青豆数 = (当前待出栈柱子高度 - preHeight) * (当前插入的柱子下标 - 待出栈的柱子下标 - 1)

3.重复上一轮的计算

4.当所有矮于当前柱子的元素都已出栈后,若此时栈内仍有元素,说明栈内元素的高度大于当前柱子,则这两根柱子间也可以攒青豆,可攒青豆数 = (当前插入的柱子高度 - preHeight) * (当前插入的柱子下标 - 栈顶柱子的下标 - 1)

代码实现(C++)

int trapB(vector<int>& height){
  int len = height.size();
  if(len <= 2)  return 0; // 当数组长度小于等于2个时,此时没有凹陷区域可以攒青豆,直接返回0

  stack<pair<int,int>> s; // 用栈来记录每个障碍物左边第一个比它高的位置,累加两个障碍物间可攒的青豆数,最后栈内元素呈严格递减排列,pair第一个参数存下标,第二个参数存高度

  int res = 0;

  for(int i = 0;i < len;i++){
    int preHeight = 0; // 记录上一次出栈的元素高度
    while(!s.empty() && s.top().second < height[i]){ // 若栈不空且栈顶元素高度小于当前障碍物高度,则此时栈顶元素需要出栈
      res += (s.top().second - preHeight) * (i - s.top().first - 1); // 出栈前可以计算出栈元素与当前障碍物之间可攒青豆值
      preHeight = s.top().second; // 记录上一次出栈的元素高度
      s.pop(); // 出栈
    }
    if(!s.empty()){ // 若经过循环后栈仍不为空,则说明此时栈顶元素高于当前障碍物,此时两个障碍物之间仍可攒青豆
      res += (height[i] - preHeight) * (i - s.top().first - 1); // 记录可攒青豆数
    }

    s.push({i,height[i]}); // 当前障碍物入栈
  }

  return res;
}

时间复杂度:O(n)

空间复杂度:O(n)

解法三:动态规划

对于下标为ii的柱子,其能否攒青豆取决于其左边最高的柱子高度与右边最高的柱子高度是否都高于当前柱子的高度,如果都高于当前柱子的高度,则该柱子可攒的青豆数为min(leftMaxHeight,rightMaxHeight)height[i]min(leftMaxHeight,rightMaxHeight) - height[i].如图所示,对于下标为2的柱子来说,其左边最高的柱子高度为4,右边最高的柱子高度为3,则可攒青豆数=min(3,4)1=2=min(3,4) - 1=2

因此需要维护两个数组,一个数组记录当前位置到最左边的最大高度,一个数组记录当前位置到最右边的最大高度,状态转移方程为leftMaxHeight[i]=max(leftMaxHeight[i1],height[i])leftMaxHeight[i] = max(leftMaxHeight[i - 1],height[i])rightMaxHeight[j]=max(rightMaxHeight[j+1],height[i])rightMaxHeight[j] = max(rightMaxHeight[j + 1],height[i])

代码实现(C++)

int trapC(vector<int>& height){
  int len = height.size();
  if(len <= 2) return 0;// 当数组长度小于等于2个时,此时没有凹陷区域可以攒青豆,直接返回0

  vector<int> leftMax(len,0); // 用于记录当前障碍物的左边第一个比它高的障碍物的高度
  vector<int> rightMax(len,0); // 用于记录当前障碍物的右边第一个比它高的障碍物的高度
  
  leftMax[0] = height[0];
  rightMax[len - 1] = height[len - 1];

  for(int i = 1;i < len;i++){
    leftMax[i] = max(leftMax[i - 1],height[i]);
  }

  for(int j = len - 1;j >= 0;j--){
    rightMax[j] = max(rightMax[j + 1],height[j]);
  }

  int res = 0;

  // 当前位置可以攒的青豆数,即为左边障碍物最大值以及右边障碍物最大值两者的较小值,减去当前位置障碍物的高度
  for(int i = 0;i < len;i++){
    res += min(leftMax[i],rightMax[i]) - height[i];
  }

  return res;
}

时间复杂度:O(n)

空间复杂度:O(n)

结果展示

测试代码

int main() {
  int len = 0;
  cin >> len;
  vector<int> height(len,0);
  for(int i = 0;i < len;i++){
    cin >> height[i];
  }
  cout << "双指针:" << trapA(height) << endl;
  cout << "单调栈:" << trapB(height) << endl;
  cout << "动态规划:" << trapC(height) << endl; 

  return 0;
}

测试用例

input: [9 5 0 2 1 4 0 1 0 3]

输出结果

总结

接雨水之青豆版,也是一道非常常见的算法题,以上是我总结出的几个接雨水常见解法。其中解法一与解法三相对来说好理解一点(也是我经常使用的解法),解法二所使用的单调栈思想并不算常见,且涉及计算部分比较绕。推荐使用解法一来解决接雨水问题,因为代码比较简单,且空间复杂度仅O(1)。

文章完整代码

代码片段:主题4:攒青豆 - 码上掘金 (juejin.cn)

感谢您的观看,希望对您有帮助,欢迎热烈的交流!
创作不易,如有帮助可点赞收藏,给予我创作动力!