输入一个二维字符类型数组,其元素取值只可能为'0'或'1'。求该数组中由'1'构成的最大正方形的面积(即包含的元素个数)。例如,如果输入如下二维数组:
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
那么,就输出4。
---------------------------
一、Brute Force
最容易想到的是一种比较直接的做法:(1)遍历该二维数组;(2)如果当前元素为’1’,就计算以该元素为左下角的能构成的最大正方形的边长;(3)最后,最大的边长的平方即为题目要求的面积。
其中第(2)中计算最大正方形边长的方法是,从该元素出发,向左向上遍历,直到遇到边界或‘0’为止。
这种方法需要逐个遍历二维数组中的元素,并且在处理每个元素时,最坏情形下需要进一步逐个遍历二维数组。因此,时间复杂度为O((R * C) ^ 2),其中R和C分别为二维数组的行数和列数;不需要辅助空间,因而空间复杂度为O(1)。
二、动态规划(DP):优化时间复杂度
在Brute Force的解法中,每当要计算以当前元素为左下角的正方形的最大面积时,都要从该元素出发,向左向上遍历二维数组,从而都对某些元素而言多次重复了之前已经做过的遍历操作。可见,该解法存在子问题的重复求解现象,而这正是DP排上用场的场合。
DP本质上是用表来存储中间状态,从而避免子问题的重复求解。另外,解决DP问题的关键在于找到状态方程。
在这个问题中,可以定义一个与输入数组matrix行列对应相等的整型数组lens。lens中某个位置的元素用于存放在matrix中以该位置为左下角的最大正方形的边长值。对于lens中位置为(i , j)的元素,其状态按如下更新规则确定:
IF matrix[i][j] == '1' THEN
lens[i][j] = min(lens[i - 1][j], lens[i][j - 1],
lens[i - 1][j - 1]) + 1;
最后,返回最大边长的平方即可。代码如下:
public int maximalSquare(char[][] matrix) {
if(matrix.length == 0) {
return 0;
}
int row = matrix.length, col = matrix[0].length;
int[][] lens = new int[row][col];
int maxlen = 0;
for(int i = 0; i < row; i++) {
for(int j = 0; j < col; j++) {
if(matrix[i][j] == '0')
continue;
int len_i_1 = i == 0 ? 0 : lens[i - 1][j],
len_j_1 = j == 0 ? 0 : lens[i][j - 1],
len_ij_1 = i == 0 || j == 0 ? 0 : lens[i - 1][j - 1];
lens[i][j] = Math.min(len_i_1, Math.min(len_j_1, len_ij_1)) + 1;
maxlen = Math.max(maxlen, lens[i][j]);
}
}
return maxlen * maxlen;
}
时间复杂度O(R * C),空间复杂度O(R *C)。对比Brute Force,相当于用空间换时间。
三、进一步优化空间复杂度
通过仔细观察DP的状态更新过程可以发现,其实在计算lens[i][j]的时候,只用到了lens中与(i, j)相邻的行和相邻的列的值,而与其它值无关。因此,可以对空间开销进一步优化,将lens的规模由R * C变为C。代码如下:
public int maximalSquare(char[][] matrix) {
if(matrix.length == 0) {
return 0;
}
int row = matrix.length, col = matrix[0].length;
int[] lens = new int[col];
int maxlen = 0;
for(int i = 0; i < row; i++) {
int pre = 0;
for(int j = 0; j < col; j++) {
if(matrix[i][j] == '0') {
lens[j] = 0;
continue;
}
int len_i_1 = i == 0 ? 0 : lens[j],
len_j_1 = j == 0 ? 0 : lens[j - 1],
len_ij_1 = pre;
pre = lens[j];
lens[j] = Math.min(len_i_1, Math.min(len_j_1, len_ij_1)) + 1;
maxlen = Math.max(maxlen, lens[j]);
}
}
return maxlen * maxlen;
}
时间复杂度O(R * C),空间复杂度O(C)。
总结:算法题求最佳解法的过程有时候是一个对已有解法不断进行优化的过程。对于一个问题,通常我们总能想到一个原理性的解法,而这种解法虽然正确但是往往时间或者空间开销不可接受。如果该解法中存在子问题重复计算的现象,那么通常可以通过DP来优化时间复杂度。DP本质上是一种用空间开销增加的代价来换取时间复杂度降低的做法,其关键的是找出状态转换方程。在用DP成功地解决了问题之后,可以通过分析DP状态转换过程所涉及的变量,看能否进一步压缩空间开销。