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

来源:互联网 发布:决胜60秒源码 编辑:程序博客网 时间:2024/05/21 11:30

破除想当然

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

题目均摘自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