# [221. Maximal Square](https://leetcode.com/problems/maximal-square/) # 思路 求一个0/1二维数组中包含的最大正方形的面积,要求这个正方形所含区域全1。 ## 思路一 暴力法一 我们遍历一遍二维数组,把数组中每个点都当成正方形的左上顶点。例如遍历到[i,j]时,边长从1开始逐渐增大搜索以matrix[i][j]为左上顶点的最大正方形边长。 时间复杂度分析: 若数组为长宽都为n,那么把第一行每个点都当成正方形的左上顶点最坏需要访问 $ n^2 + (n-1)^2 + ... + 1^2 \sim O(n^3) $次数组, 所以总的复杂度为O(n^4)级别。 > 虽然看起来这个复杂度很高,但是实测其实也挺快的。 ## 思路二 暴力法二 还有一种暴力法,也是遍历一遍数组,与思路一不同的是这次把每个点当做正方形的右下顶点。我们首先需要动态规划地计算一个累加数组sum,sum[i][j]代表matrix数组在[i,j]及其左上方区域的和(即矩形区域matrix[0~i, 0~j]),则有`sum[i][j] = sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]`。sum计算好后就可以方便搜索以matrix[i][j]为右下顶点的最大正方形了,方法和计算sum数组的思想类似,就是从1开始不断增加正方形边长len, 根据sum计算左上顶点matrix[i-len][j-len]与右下顶点matrix[i][j]构成的正方形区域的元素和是否等于`len*len`,若是则说明这个方形区域全1。 时间复杂度O(n^3), 空间复杂度O(n^2)。 ## 思路三 动态规划 前面两种复杂度都比较高,我们可以用动态规划的方法将复杂度降至O(n^2)级别。用一个二维数组dp, dp[i][j]表示以matrix[i][j]为右下角的全1正方形区域的最大边长。那么如何更新dp呢?有以下几种情况: * 若`matrix[i][j] == '0'`,那dp[i][j]肯定为0; * 否则,若`i == 0 || j == 0`,那么dp[i][j]为1; * 否则,`dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j], dp[i][j-1])) + 1`(可以画个图帮助理解)。 在更新过程中不断记录最大边长sqrt_res,最后返回其平方即可。 时空复杂度均为O(n^2)。 ## 思路四 动态规划空间改进 仔细分析思路三中dp的更新过程我们可以发现其实不用开辟一个二维数组而只需要一个一维数组就可以了。(这是动归常见空间改进思路) 我们用一个一维数组dp, 然后从左向右从上往下遍历matrix,若此时刚结束第i-1行的遍历,则此时dp[j]表示思路三的dp[i-1][j],那在遍历第i行的过程中如何更新dp呢?我们需要用到辅助变量cur来表示dp[j]的应该更新的值(先用cur记录,但先不更新dp[j]),用pre代表上次循环的cur,即pre代表dp[j-1]应该更新的值,则思路三中的更新策略对应为: * 若`matrix[i][j] == '0'`,则cur肯定为0; * 否则,若`i == 0 || j == 0`,那么cur为1; * 否则,`cur = min(dp[j-1], min(dp[j], pre)) + 1`。 * 最后我们再更新`dp[j-1] = pre`以及`pre = cur`。 注意每一行循环完毕之后需要更新一下dp最后一个元素为cur,因为在循环中没更新的。 时间复杂度O(n^2), 空间复杂度O(n)。 ## 思路五、转换成求直方图包含的最大正方形 这题和[85. Maximal Rectangle](https://leetcode.com/problems/maximal-rectangle/)有点像,不同点就是85题只需要矩形而本题要求区域是正方形。如果把二维矩阵的第 i 行及上面的部分可以看成是一个直方图,那么85题又转化成了[84. Largest Rectangle in Histogram](https://leetcode.com/problems/largest-rectangle-in-histogram/)(见[85题思路一](85.%20Maximal%20Rectangle.md))。顺着这个思路,我们也可以利用84题来解此题。 具体来说,我们把二维矩阵的第 i 行及上面的部分可以看成是一个直方图(假设用数组hs表示),然后采用[84思路一](84.%20Largest%20Rectangle%20in%20Histogram.md)求出直方图里包含的最大正方形: 求出`hs[i]`向左第一个比他小的数`hs[j1]`和向右第一个比他小的数`hs[j2]`。那么此时能容纳的正方形的边长就为高和宽的较小者,即`min(heights[i], j2 - j1 - 1)`。 求数组中每个元素的左(右)边第一个比他大(小)的元素可利用单调栈在O(n)时间内求出,关于单调栈可以参考[我的总结](../algorithm/array/monotonic_stack_queue.md)。 总的时间复杂度O(n^2), 空间复杂度O(n)。 ## 思路一 ``` C++ class Solution { public: int maximalSquare(vector >& matrix) { if (matrix.empty() || matrix[0].empty()) return 0; int sqrt_res = 0; int m = matrix.size(), n = matrix[0].size(); for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if(matrix[i][j] == '0') continue; int len = 1, max_len = min(m - i, n - j); if(max_len < sqrt_res) continue; // 剪枝 // 逐渐增大边长 for(; len <= max_len; len++){ if(i + len >= m || j + len >= n) break; int flag = 1; for(int k = 0; flag && k <= len; k++) if(matrix[i+len][j+k] == '0') flag = 0; for(int k = 0; flag && k < len; k++) if(matrix[i+k][j+len] == '0') flag = 0; if(!flag) break; } sqrt_res = max(sqrt_res, len); } } return sqrt_res * sqrt_res; } }; ``` ## 思路二 ``` C++ class Solution { public: int maximalSquare(vector >& matrix) { if (matrix.empty() || matrix[0].empty()) return 0; int sqrt_res = 0; int m = matrix.size(), n = matrix[0].size(); vector>sum(m, vector(n, 0)); for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { int cur_sum = matrix[i][j] - '0'; if(i > 0) cur_sum += sum[i-1][j]; if(j > 0){ cur_sum += sum[i][j-1]; if(i > 0) cur_sum -= sum[i-1][j-1]; } sum[i][j] = cur_sum; int len, max_len = min(i, j) + 1; // len从sqrt_res + 1开始以剪枝 for(len = sqrt_res + 1; len <= max_len; len++){ int area = cur_sum; if(i - len >= 0) area -= sum[i-len][j]; if(j - len >= 0){ area -= sum[i][j - len]; if(i - len >= 0) area += sum[i-len][j-len]; } if(area == len*len) sqrt_res = max(sqrt_res, len); } } } return sqrt_res * sqrt_res; } }; ``` ## 思路三 ``` C++ class Solution { public: int maximalSquare(vector >& matrix) { if (matrix.empty() || matrix[0].empty()) return 0; int sqrt_res = 0; int m = matrix.size(), n = matrix[0].size(); vector>dp(m, vector(n, 0)); for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if( matrix[i][j] == '0') continue; if(i == 0 || j == 0) dp[i][j] = 1; else dp[i][j] = 1 + min(dp[i-1][j-1], min(dp[i-1][j], dp[i][j-1])); sqrt_res = max(sqrt_res, dp[i][j]); } } return sqrt_res * sqrt_res; } }; ``` ## 思路四 ``` C++ class Solution { public: int maximalSquare(vector >& matrix) { if (matrix.empty() || matrix[0].empty()) return 0; int sqrt_res = 0, pre, cur; int m = matrix.size(), n = matrix[0].size(); vectordp(n, 0); for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if(matrix[i][j] == '0') cur = 0; else if(i == 0 || j == 0) cur = 1; else cur = 1 + min(dp[j-1], min(dp[j], pre)); if(j > 0) dp[j-1] = pre; pre = cur; sqrt_res = max(sqrt_res, cur); } // 每一行循环完毕之后需要更新一下dp最后一个元素 dp[n - 1] = cur; } return sqrt_res * sqrt_res; } }; ``` ## 思路五 ``` C++ class Solution { private: int helper(const vector&hs){ // 求直方图能容纳的最大正方形, 类似84题 int n = hs.size(), max_l = 0; vectorpre_smaller(n, -1), next_smaller(n, n); // 存的是下标 stackstk1, stk2; // 存的也是下标 for(int i = 0; i < n; i++){ while(!stk1.empty() && hs[stk1.top()] >= hs[i]) stk1.pop(); if(!stk1.empty()) pre_smaller[i] = stk1.top(); stk1.push(i); int j = n - i - 1; // 相当于反向遍历: for(int j = n-1; j >= 0; j--) while(!stk2.empty() && hs[stk2.top()] >= hs[j]) stk2.pop(); if(!stk2.empty()) next_smaller[j] = stk2.top(); stk2.push(j); } for(int i = 0; i < n; i++) max_l = max(max_l, min(hs[i], next_smaller[i] - pre_smaller[i] - 1)); return max_l * max_l; } public: int maximalSquare(vector>& matrix) { if(matrix.empty()) return 0; int m = matrix.size(), n = matrix[0].size(), res = 0; vectorhs(n, 0); for(int i = 0; i < m; i++){ for(int j = 0; j < n; j++){ if(matrix[i][j] == '1') hs[j]++; else hs[j] = 0; } res = max(res, helper(hs)); } return res; } }; ```