青训营主题活动——攒青豆解法

109 阅读4分钟

当青训营遇上码上掘金

主题4:攒青豆

题目:

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

攒青豆.png

以下为上图例子的解析:

输入:height = [5,0,2,1,4,0,1,0,3]
输出:17
解析:上面是由数组 [5,0,2,1,4,0,1,0,3] 表示的柱子高度,在这种情况下,可以接 17 个单位的青豆。

方式一:暴力循环

性能:image-20230211185514097

解题思路:

每根主子的存水量 = 该柱子的左右两侧最大高度的较小者减去此柱子的高度。只要能找到每根柱子的左右两侧的最大值就能求出当前柱子的接水量。

代码如下:

/**
     * 使用最暴力循环求解
     * @param arr
     * @return
     */
    public static int getSum(int[] arr) {
        int length = arr.length;
        int result = 0;
        for (int i = 0; i < length; i++) {
            int leftMax = 0, rightMax = 0;
//            找到左边最高的柱子
            for (int j = 0; j < i; j++) {
                leftMax = Math.max(leftMax, arr[j]);
            }
//            找到右边最高的柱子
            for (int j = i + 1; j < length; j++) {
                rightMax = Math.max(rightMax, arr[j]);
            }
//            得到左右最高柱子中较小的一个
            int min = Math.min(leftMax, rightMax);
//            判断柱子本身是否低于较小值min,满足则可以接到青豆:result+=min-arr[i]
            if (min > arr[i]) {
                result += min - arr[i];
            }
        }
        return result;
    }

测试:

 public static void main(String[] args) {
        int[] arr = {5, 0, 2, 1, 4, 0, 1, 0, 3};
        System.out.println("暴力循环:");
        System.out.println(getSum(arr));
 }

image-20230211185253047

方式二:动态规划

性能:image-20230211185557356

解题思路:

在上述的暴力法中,对于每个柱子,我们都需要从两头重新遍历一遍求出左右两侧的最大高度,这里是有很多重复计算的, 很明显最大高度是可以记忆化的,具体在这里可以用数组边递推边存储,也就是常说的动态规划。

具体做法:

定义二维dp数组 int[][] dp = new int[n] [2],其中, dp[i] [0] 表示下标i的柱子左边的最大值, dp[i] [1] 表示下标i的柱子右边的最大值。 分别从两头遍历数组,为 dp[i] [0]和 dp[i] [1] 赋值。 同方法1,遍历每个柱子,累加每个柱子可以储水的高度

代码如下:

    /**
     * 使用动态规划
     * @param arr
     * @return
     */
    public static int getSum1(int[] arr) {
        int result = 0;
        int length = arr.length;
        /*
        * dp存储每个柱子的左边最大值和右边最大值
        * */
        int[][] dp = new int[length][2];
//        寻找左边最大值
        dp[0][0] = arr[0];
        for (int i = 1; i < length; i++) {
            dp[i][0] = Math.max(dp[i - 1][0], arr[i]);
        }
//        寻找右边最大值
        dp[length - 1][1] = arr[length - 1];
        for (int i = length - 2; i >= 0; i--) {
            dp[i][1] = Math.max(dp[i + 1][1], arr[i]);
        }
//        统计最后结果
        for (int i = 0; i < length; i++) {
            result += Math.min(dp[i][0], dp[i][1]) - arr[i];
        }
        return result;
    }

测试:

 public static void main(String[] args) {
        int[] arr = {5, 0, 2, 1, 4, 0, 1, 0, 3};
        System.out.println("动态规划:");
        System.out.println(getSum1(arr));
 }

image-20230211185118581

方式三:单调栈

性能:image-20230211185822217

解题思路:

单调栈就是比普通的栈多一个性质,即维护栈内元素单调

​ 简单来说就是当前柱子如果小于等于栈顶元素,说明形不成凹槽,则将当前柱子入栈; 反之若当前柱子大于栈顶元素,说明形成了凹槽,于是将栈中小于当前柱子的元素pop出来,将凹槽的大小累加到结果中。比如当前某个单调递减额栈的元素从栈底到栈顶分别是:[10,9,8,3,4],如果要入栈元素 5,需要把栈顶元素pop出去,直到满足单调递减为止,即先变成[10,9,8],再入栈5,就[10,9,8,5]。

代码如下:

/**
 * 单调栈实现
 */
public static int getValue(int[] arr) {
    Stack<Integer> stack = new Stack<>();
    int result = 0;
    boolean isInit = false;
    int left = 0;
    for (int i = 0; i < arr.length; i++) {
        /*
        * 利用isInit 判断栈是否初始化,并初始化栈
        * */
        if (!isInit && arr[i] != 0) {
            stack.push(arr[i]);
            left = i;
            isInit = true;
            continue;
        }
        if (!isInit) {
            continue;
        }
        /*
        * 发现新元素大于栈底元素(左边界),弹栈做统计,重新定义左边界
        * */
        if (arr[i] >= arr[left]) {
            while (!stack.isEmpty()) {
                result += arr[left] - stack.pop();
            }
            left = i;
            stack.push(arr[i]);
        } else {
            stack.push(arr[i]);
        }
    }
    /*
    * 找到最高点,逆序再做一次单调栈统计
    * */
    isInit = false;
    int right = 0;
    stack.clear();
    for (int i = arr.length - 1; i >= left; i--) {
        if (!isInit && arr[i] != 0) {
            stack.push(arr[i]);
            right = arr[i];
            isInit = true;
            continue;
        }
        if (!isInit) {
            continue;
        }
        if (arr[i] >= right) {
            while (!stack.isEmpty()) {
                result += right - stack.pop();
            }
            right = arr[i];
            stack.push(arr[i]);
        } else {
            stack.push(arr[i]);
        }
    }
    return result;
}

测试:

public static void main(String[] args) {
        int[] arr = {5, 0, 2, 1, 4, 0, 1, 0, 3};
        System.out.println("单调栈:");
        System.out.println(getValue(arr));
    }

image-20230211190715821

方式四:双指针

性能:image-20230211190815713

解题思路:

在上述的动态规划方法中,我们利用二维数组来存储每个柱子左右两侧的最大高度,但我们递推累加每个柱子的储水高度时只用到来 do[i][0]dp[i][1] 两个字,因此我们递推的时候只需要用 int leftMaxint rightMax 两个变量就行了。这里的leftMax 是从左端开始递推得到的,而 rightMax 是从右端开始递推得到的。因此遍历每个柱子,累加每个柱子的储水高度时,也需要用 left 和 right 两个指针从两端开始遍历。

代码如下:

public static int getSum2(int[] arr) {
    int result = 0;
    if (arr == null || arr.length == 0) {
        return result;
    }
    
    int left = 0, right = arr.length - 1;
    // 定义两个指针
    int leftMax = 0, rightMax = 0;
    while (left <= right) {
        // 左端最高柱子小于右端最高柱子,保证右边界,处理左边界。
        if (leftMax <= rightMax) {
            // 与当前柱子比较去较大的
            leftMax = Math.max(leftMax, arr[left]);
            result += leftMax - arr[left];
            left++;
        } else {
            rightMax = Math.max(rightMax, arr[right]);
            result += rightMax - arr[right];
            right--;
        }
    }
    return result;
}

测试:

public static void main(String[] args) {
    int[] arr = {5, 0, 2, 1, 4, 0, 1, 0, 3};
    System.out.println("双指针:");
    System.out.println(getSum2(arr));
}

image-20230211192407697

总结:

​ 总体性能双指针最优,但单调栈的思想十分重要。