算法细节系列(12):破除想当然

破除想当然

总结最近遇到的一些有趣题。知识点主要有【动态规划】,【栈】,【数组】,【状态记录】,这些题目挺有趣,主要原因在于写程序时,需要破除想当然的人类求解过程,而是回归到低级的计算机思维,一步步教计算机怎么做。

题目均摘自leetcode,分为以下三题(求面积系列)。

  • 84 Largest Rectangle in Histogram
  • 85 Maximal Rectangle
  • 221 Maximal Square

84 Largest Rectangle in Histogram

之前刚好总结了博文【next Greater Element系列】,以下内容将用到该博文讲到的知识点,可以先去瞧瞧,方便理解。

Problem:

Given n non-negative integers representing the histogram’s bar height where the width of each bar is 1, find the area of largest rectangle in the histogram.

alt text
Above is a histogram where width of each bar is 1, given height = [2,1,5,6,2,3].

alt text
The largest rectangle is shown in the shaded area, which has area = 10 unit.

For example,
Given heights = [2,1,5,6,2,3],
return 10.

暴力做法:针对每个元素,遍历一遍数组,找出最大的宽度,求出面积。即使这样答案也不那么显而易见,这里就不贴出代码了。我们再来看看另一种思路。

先考虑人类是如何思考这问题的,当我看到这图的第一眼,我就扫出了答案,面积不就是10么。可你想过你大脑是怎么工作的么?

两点:状态记录+回溯求解,这些方法在你脑中一闪而过,但在计算机视角中没那么简单,要想理清楚得费一些周折。

简单来说,我们在扫面积的时候,当遍历一个元素后,我们已经把先前状态记录在脑海中,所以在对下一个元素比较时,我们自然有了之前的信息,但观察很多for循环语句你会发现,它们均无状态记录,遍历一遍就过去了,这都是暴力的做法。所以从中就能得到一些优化的细节,如在next Greater Element系列中,提到的栈,就是为了保存之前的路径而存在。

那么该题目的思路是什么?其实再思考一步,答案就出来了,如当我们扫到第二个元素1时,我们怎么求它的面积?不就是当前最小高度乘以宽度2,答案是2么?没错,在你做的过程中,你天然的把第一个元素的信息利用上去了(高度的比较);其次,计算面积时,你把第一个元素的多余部分给切除了。

所以,现在的过程就很简单了,每当遍历到一个新元素时,不管三七二十一,把它放入一个容器中,记录当前状态,当我遍历到下一个元素时,出现两种情况咯,要么比它大,要么比它小。比它小的之前已经说过了,计算面积时,把前一个元素高出的部分切除,计算面积。比它大的咋办呢?暂时放着呗,这点也是人容易忽略的步骤,假想我们不知道数组的长度,但数组是不断递增的,你要如何得到整个数组的面积?你肯定得看完所有数组啊!而且你也知道,只要当它不断递增,那么从刚开始递增的那个元素开始,它一定是最大面积。所以你有必要等到数组开始递减为止,是吧。

好了,说了那么多,再理理思路,代码就能出来了,直接上代码。

public int largestRectangleArea(int[] heights) {

        Stack<Integer> stack = new Stack<>();

        int max = 0;
        //注意 边界条件的处理
        for (int i = 0; i <= heights.length; i++){
            int curr = (i == heights.length) ? 0: heights[i];
            // 注意  count 和 heights[pos] = curr
            int count = 0;
            while(!stack.isEmpty() && heights[stack.peek()] > curr){
                int pos = stack.pop();
                max = Math.max(max, heights[pos] * (i - pos));
                heights[pos] = curr;
                count++;
            }
            i -= count;
            stack.push(i);
        }

        return max; 
    }

这道题用到了stack记录遍历状态。注意的地方比较多,咋说呢,如果就着自己的思路,代码可以五花八门,所以不必纠结太多细节的东西。

  1. 当前元素小于栈顶元素时,把大于当前元素的元素下标给pop出来,为啥咧,因为我们要开始计算这些递增的面积了,每计算一次,就维护一个最大的max。
  2. 啥时候停止咧,当栈的元素小于当前元素时,停止计算,因为此时,你又回到了不断递增的状态,你得继续输入数组元素了。
  3. 那么问题来了,已经被pop的元素咋办呢,刚才说了,切除多于的部分,意思就是让它们和当前元素的高度相等,它不会影响后续计算。
  4. 此时,这些元素得重新加入栈中,所以有了count计数,把i指定到被切除元素的最后一个下标。
  5. 那么,当数组输入完毕后,栈中还有元素咯?且它们都是递增的,没错。所以有了特殊的循环,遍历数组长度+1次,就是为了在数组最后加个边界处理,很巧妙,元素为0时,一定能把栈中所有元素给吐出来。

所以说,人高级啊,想都不用想直接就能出答案,这反而导致了我们在教计算机做题目时成了一个很大的障碍。呵呵,继续吧。

85 Maximal Rectangle

Problem:

Given a 2D binary matrix filled with 0’s and 1’s, find the largest rectangle containing only 1’s and return its area.

For example, given the following matrix:

1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0

Return 6.

嗯哼,如果有了前面那道题的铺垫,这道题就不难了。但怪就怪在,如果没有前面那道题,你就很难想到!就因为被它的矩阵吓到了?这些认知障碍得破除啊。

它是一个平面,让你求最大长方形面积,所以呢,想法就是把矩阵一层一层往下切。先切出第一层,我们能计算每个位置的高度,为1 0 1 0 0,然后切第二层2 0 2 1 1,第三层3 1 3 2 2,第四层4 0 0 3 0,这不就是分别求每一层的面积么,数值就是它们的高度,不用解释。

接下来的问题,就是生成这样的矩阵就好了,很简单,一个dp就能搞定。这不能算严格意义上的dp,但题目说是dp,那就dp吧,所以代码如下:

public int maximalRectangle(char[][] matrix) {

        int row = matrix.length;
        if (row == 0)
            return 0;
        int col = matrix[0].length;
        if (col == 0)
            return 0;

        int[][] w = new int[row + 1][col + 1];

        for (int j = 1; j < col + 1; j++) {
            if (matrix[0][j - 1] == '1') {
                w[1][j] = 1; // 宽
            }
        }

        for (int i = 2; i < row + 1; i++) {
            for (int j = 1; j < col + 1; j++) {
                if (matrix[i - 1][j - 1] == '1') {
                    w[i][j] = w[i-1][j] + 1;
                }
            }
        }

        int max = 0;
        for (int i = 1; i < row+1; i++){
            max = Math.max(max,largestRectangleArea(w[i]));
        }

        return max;
    }

    public int largestRectangleArea(int[] heights) {

        Stack<Integer> stack = new Stack<>();

        int max = 0;
        for (int i = 0; i <= heights.length; i++) {
            int curr = (i == heights.length) ? 0 : heights[i];
            int count = 0;
            while (!stack.isEmpty() && heights[stack.peek()] > curr) {
                int pos = stack.pop();
                max = Math.max(max, heights[pos] * (i - pos));
                heights[pos] = curr;
                count++;
            }
            i -= count;
            stack.push(i);
        }

        return max;
    }

思路很清楚,没什么好解释的,继续下一题。

221 Maximal Square

Problem:

Given a 2D binary matrix filled with 0’s and 1’s, find the largest square containing only 1’s and return its area.

For example, given the following matrix:

1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0

Return 4.

呵呵,如果就着前面的思路,那么就完蛋了,反正我是又做不出来了。这道题的思路比较奇葩,是一道较难的dp题。咋说呢,我其实还没掌握它的核心思想。用的是正方形生成性质,很难理解它为啥是正确的。

状态转移方程:
dp[i+1][j+1] = Math.min(dp[i][j+1], Math.min(dp[i+1][j], dp[i][j])) + 1;

其中 i和j,分别表示在当前坐标(i,j)能够生成最大矩形的长。
alt text

所以说新的正方形,一定是那三个状态的最小值,否则不可能构成一个更大的正方形,自己笔画下。

public int maximalSquare(char[][] matrix) {
        if (matrix.length == 0 || matrix[0].length == 0) return 0;

        int n = matrix.length, m = matrix[0].length;

        int[][] dp = new int[n+1][m+1];

        int max = 0;
        for(int i = 0; i < n; i++){
            for (int j = 0; j < m; j++){
                if (matrix[i][j] == '1'){
                    dp[i+1][j+1] = Math.min(dp[i][j+1], Math.min(dp[i+1][j], dp[i][j])) + 1;
                    max = Math.max(max, dp[i+1][j+1]);
                }
            }
        }


        return max * max;
    }

这道题给了我dp的一个新思路,不一定dp要记录每一步的最优解,即dp到最后不一定就是本题的答案,相反,我们可以在dp更新的时候,时刻更新max,那么求解它的思路和想法就广了很多。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值