「青训营 X 码上掘金」-- 攒青豆的三种解题思路

57 阅读2分钟

当青训营遇上码上掘金!

让我们一起快乐写码赢青训营青豆吧!

主题介绍

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

输入输出

输入:
    9
    5 0 2 1 4 0 1 0 3
输出:
    17
解析:第一行输入表示柱子的总位置长度,第二行输入表示各个位置柱子的高度;
     上面是由数组 [5,0,2,1,4,0,1,0,3] 表示的柱子高度,在这种情况下,可以接 17 个单位的青豆。

解题思路

刷题较多的小伙伴应该能一眼看出本题就是经典题目--接雨水,雨水换成豆子,换汤不换药

1.暴力解法

本题可以转换为逐一计算每个位置上能接到青豆的数量,该数量取决于左右柱子最大高度的较小值,减掉柱子高度后即为青豆数量,求和即为总青豆数量。

/**
* 支持 import Java 标准库 (JDK 1.8)
*/
import java.util.*;
/**
* 注意:目前 Java 代码的入口类名称必须为 Main(大小写敏感)
*/
public class Main {
    // 可以攒住青豆的最少柱子位置数量
    private static final int MinNum = 3;
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 柱子摆放位置数量
        int size = scanner.nextInt();
        if (size < MinNum){
            System.out.println(0);
            return;
        }
        int[] height = new int[size];
        for (int i = 0; i < size; i++) {
            height[i] = scanner.nextInt();
        }
        int ans = 0;
        for (int i = 1; i < size-1; i++){
            int max_left = 0;
            int max_right = 0;
            for (int j = i; j >= 0; j--){
                max_left = Math.max(max_left, height[j]);
            }
            for (int j = i; j < size; j++){
                max_right = Math.max(max_right, height[j]);
            }
            ans += Math.min(max_left, max_right) - height[i];
        }
        System.out.println(ans);
    }
}

2.动态规划

暴力解中,计算每一列能攒的青豆数量时,都是重新遍历一遍所有高度,存在大量重复操作,可以通过状态矩阵来优化遍历过程。

dp_left[i]表示第i列左边最高的柱子的高度,dp_right表示第i列最高的柱子的高度。

状态转移公式为:

  • dp_left [i] = Max(dp_left [i-1],height[i-1])
  • dp_right[i] = Max(dp_right[i+1],height[i+1])

公式一表示当前位置左边最高柱子的高度是左边相邻柱子的左边的最高高度和左边相邻柱子的高度中最大的高度。公式二表示当前位置右边最高柱子的高度是右边相邻柱子的右边的最高高度和右边边相邻柱子的高度中最大的高度。

这样改进的暴力解法中,不用在循环里每次重新遍历来计算max_leftmax_right

public class Main {
    // 可以攒住青豆的最少柱子位置数量
    private static final int MinNum = 3;
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 柱子摆放位置数量
        int len = scanner.nextInt();
        if (len < MinNum){
            System.out.println(0);
            return;
        }
        int[] height = new int[len];
        for (int i = 0; i < len; i++) {
            height[i] = scanner.nextInt();
        }
        int ans = 0;
        // dp[i]表示当前位置左边或右边的最高柱子的高度
        int[] dp_left = new int[len];
        int[] dp_right = new int[len];
        // 初始化边界条件
        dp_left[0] = height[0];
        dp_right[len-1] = height[len-1];
        // 状态转移
        for (int i = 1; i < len; i++) {
            dp_left[i] = Math.max(dp_left[i-1], height[i]);
        }
        for (int i = len-2; i >= 0; i--){
            dp_right[i] = Math.max(dp_right[i+1], height[i]);
        }
        // 最小高度和当前高度的差值
        for (int i = 0; i < len; i++) {
            ans += Math.min(dp_left[i], dp_right[i]) - height[i];
        }
        System.out.println(ans);*/

3.单调栈

通过维护一个单调非严格递减栈,从前往后遍历每个柱子高度,若当前的柱子高度小于等于栈顶柱子高度,则直接入栈;若大于栈顶柱子高度,则代表必然会有积攒青豆,因为栈顶柱子下面还有比它高的柱子,可以形成一个凹陷区域积攒青豆。 实现过程中需要注意:

  • 单调栈中存储的是柱子的位置索引
  • 每次处理凹陷区域需要处理相同高度为凹陷区域
public class Main {
    // 可以攒住青豆的最少柱子位置数量
    private static final int MinNum = 3;
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 柱子摆放位置数量
        int len = scanner.nextInt();
        if (len < MinNum){
            System.out.println(0);
            return;
        }
        int[] height = new int[len];
        for (int i = 0; i < len; i++) {
            height[i] = scanner.nextInt();
        }
        // 高度递减栈(存的是高度值的索引)
        Stack<Integer> stack = new Stack<>();
        int qinDou = 0;
        // 从左向右遍历
        for (int i = 0; i < len; i++) {
            while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
                int popNum = stack.pop();
                while (!stack.isEmpty() && height[popNum] == height[stack.peek()]){
                    stack.pop();
                }
                if (!stack.isEmpty()){
                    int leftIndex = stack.peek();
                    int hig = Math.min(height[leftIndex], height[i])-height[popNum];
                    int wid = i - leftIndex - 1;
                    qinDou += hig*wid;
                }
            }
            stack.push(i);
        }
        System.out.println(qinDou);
    }
}