算法细节系列(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.
Above is a histogram where width of each bar is 1, given height = [2,1,5,6,2,3].
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记录遍历状态。注意的地方比较多,咋说呢,如果就着自己的思路,代码可以五花八门,所以不必纠结太多细节的东西。
- 当前元素小于栈顶元素时,把大于当前元素的元素下标给pop出来,为啥咧,因为我们要开始计算这些递增的面积了,每计算一次,就维护一个最大的max。
- 啥时候停止咧,当栈的元素小于当前元素时,停止计算,因为此时,你又回到了不断递增的状态,你得继续输入数组元素了。
- 那么问题来了,已经被pop的元素咋办呢,刚才说了,切除多于的部分,意思就是让它们和当前元素的高度相等,它不会影响后续计算。
- 此时,这些元素得重新加入栈中,所以有了count计数,把
i
指定到被切除元素的最后一个下标。 - 那么,当数组输入完毕后,栈中还有元素咯?且它们都是递增的,没错。所以有了特殊的循环,遍历数组长度+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 0Return 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 0Return 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)
能够生成最大矩形的长。
所以说新的正方形,一定是那三个状态的最小值,否则不可能构成一个更大的正方形,自己笔画下。
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,那么求解它的思路和想法就广了很多。
- 算法细节系列(12):破除想当然
- 算法细节系列(1):Java swap
- 算法细节系列(13):买卖股票
- 算法细节系列(21):贪心有理?
- 算法细节系列(23):回溯
- 算法细节系列(25):加减乘除
- 算法细节系列(26):区间
- 算法细节系列(28):线段树
- 算法细节系列(29):any sum
- 算法细节系列(30):接口设计
- 算法细节系列(31):链表
- 算法细节系列(15):Valid Parentheses系列
- 算法细节系列(20):Word Ladder系列
- 想当然
- 算法细节系列(33):再见字符串(1)
- 算法细节系列(34):再见字符串(2)
- 算法细节系列(2):231.Power of Two && Three
- 算法细节系列(4):二分查找总结
- 有关easyUI的拖动操作中droppable,draggable用法例子
- web本地存储(localStorage、sessionStorage)
- PCIE_DMA实例一:xapp1052详细使用说明
- System::String转换为std::string
- 创建登录用户并授权
- 算法细节系列(12):破除想当然
- ZOJ 3958 Cooking Competition【】
- CG类型
- Virtualbox 下配置高版本Linux内核的增强包安装
- MySQL数据安全技术
- MySQL日常总结
- PHP面试笔试细节
- Android 开发中如何用Javah命令生成JNI头文件
- 字体应用