http://blog.csdn.net/pipisorry/article/details/39048485
探讨几个和求最大长方形相关的题目,并说明如何把一些相对复杂的问题化归成简单的易解的问题。这里的最大,可以指长方形内所有元素之各最大,也可以指面积最大。
问题一(最大和子矩阵) : 有一个 m x n 的矩阵,矩阵的元素可正可负。请找出该矩阵的一个子矩阵(方块),使得其所有元素之和在所有子矩阵中最大。(问题来源:http://acm.pku.edu.cn/JudgeOnline/problem?id=1050)
问题二( 最大 0/1 方块) :有一个 m x n 的矩阵,元素为 0 或 1。一个子矩阵,如果它所有的元素都是 0, 或者都是 1,则称其为一个 0-聚类 或 1-聚类,统称聚类(Cluster)。请找出最大的聚类(元素最多的聚类)(面试题)
这两个问题,除了都是在矩阵上操作之外,似乎没有什么共同之处。其实不然。事实上,它们可以用同一个思路解决。该思路来源于下面的一个问题,具体地说,就是把前两个问题化归成多个问题三:
问题三(和最大的段) :有 n 个有正有负的数排成一行,求某个连续的段,使得其元素之和最大。(问题来源:某面试题。事实上,这也是一道经典题目,具体参考 http://en.wikipedia.org/wiki/Maximum_subarray_problem)
问题四(最大长方形) : 有一个有 n 个项的统计直方图,假定所有的直方条 (bar) 的宽度一样。在所有边与 x 轴 和 y 轴平行的长方形中,求该被该直方图包含的面积最大的长方形。(问题来源:面试经典题目)
问题四似乎和前三个问题毫不相关。令人吃惊地是,它的解决方法可以给最大0/1方块问题提供新思路,同样的,就是把最大0/1方块问题化归成多个问题四。本文的重点在于化归的思想 ,即把一些相对难的问题化归成一个或多个相对容易的问题,而这些容易的问题往往有更高效和优美的解。如果化归得当,问题的解会比直接去解该问题量多优美和更有效率。
接下来的部分,我们先用动态规划解前两个问题,以给后面不同的解决思路提供对比。接着解决问题三,并介绍如何把前两个问题化归到问题三。最后解决最后一个问题,并应用它来给出问题二另一种思路。为了讨论方便,我们假设 m 和 n 只相差常数倍。否则的话,由于问题的对称性,我们可以翻转矩阵,从而使得时间复杂度取得最小。
I 前两个问题的动态规划解
最大和子矩阵问题:
每个子矩阵由列长、行长和左上角的元素位置决定。如果我们指定左上角的元素位置 (i,j) 和列长 c,那么可以求所有这些子矩阵中和最大的。然后,变化列长 c,可以求以 (i,j) 为左上角的最大和子矩阵。最所有左上角位置再求最大和子矩阵,问题就解决了。令 OPT(i,j,c) 表示以 (i,j) 为左上角,列长为 c 的最大和子矩阵之和,OPT(i,j) 表示以 (i,j) 为左上角的最优解,而 S(i,u,v) 表示第 i 行中从列 u 到列 v所有元素之和。则
OPT(i,j,c) = { OPT(i+1,j,c) + S(i,j,j+c-1)OPT(i+1,j,c) > 0
S(i,j,j+c-1)OPT(i+1,j,c) <= 0
OPT(i,j) = max { OPT(i,j,c) : 1 <= c <= n }
其中,j+c-1 <= n。当 i >m 时, OPT(i,j,c) = 0。一共有 O(mn) 个 OPT(i,j) 子问题,而每个 OPT(i,j) 又可以有 n 个决策,因此,总的解规模有 O(mn2 ) 个 OPT(i,j,c)。每个这样的子问题可以在 O(1) 时间内解决,时间复杂度为 O(mn2 )。
对于最大 0/1 块问题,可以用类似的动态规划求解。首先只考虑 1-聚类。令 OPT(i,j,c) 表示以 (i,j) 为左上角的列长为 c 的最大 1-聚类。则, OPT(i,j,c) =
1) 0,如果 (i,j) 为 0,或者 (i,j), (i,j+1),..., (i,j+1-c) 不全为 1;否则
2) OPT(i+1,j,c) + c。
然后再考虑 0-聚类,过程类似。总的时间复杂度也是 O(mn2 )。【这种解法虽然可行,效率也还可以,但状态比较多,而且也不够优雅,状态的构造比较生硬。】
/* DP算法 O(N^2*M) AC 47MS*/ static int maxSubmatrixSum2(int **a, int n){ int ***row_sum; assert( row_sum = (int ***)malloc(sizeof(int **) * n) ); for(int i = 0; i < n; i++) assert( row_sum[i] = (int **)malloc(sizeof(int *) * n) ); for(int i = 0; i < n; i++) for(int j = 0; j < n; j++){ assert( row_sum[i][j] = (int *)malloc(sizeof(int) * n) ); memset(row_sum[i][j], 0, sizeof(int) * n); //初始化row_sum[i][j][c]为0!!! } //计算row_sum[i][j][c]为第i行j列到c列的和 for(int i = n - 1; i >= 0; i--){ for(int j = 0; j < n; j++){ for(int c = j; c < n; c++) if(c == 0) row_sum[i][j][c] = a[i][c]; else row_sum[i][j][c] = row_sum[i][j][c - 1] + a[i][c]; } } //将row_sum[i][j][c]转换成第i行j列到c列的和的最优解 for(int i = n - 2; i >= 0; i--){ //row_sum[n-1][j][c]不变 for(int j = 0; j < n; j++) for(int c = j; c < n; c++){ if(row_sum[i+1][j][c] > 0) row_sum[i][j][c] += row_sum[i+1][j][c]; } } //求以[i, j]为左上角的矩形最优解 int **optij = (int **)malloc(sizeof(int *) * n); for(int i = 0; i < n; i++) optij[i] = (int *)malloc(sizeof(int) * n); for(int i = 0; i < n; i++) memset(optij[i], INT_MIN, sizeof(int) * n); for(int i = 0; i < n; i++){ for(int j = 0; j < n; j++) for(int c = j; c < n; c++){ if(row_sum[i][j][c] > optij[j][j]) optij[j][j] = row_sum[i][j][c]; } } //求整体最优解 int max_sum = INT_MIN; for(int i = 0; i < n; i++){ for(int j = 0; j < n; j++){ if(optij[i][j] > max_sum) max_sum = optij[i][j]; } } return max_sum; }
II 和最大的段问题
这个问题,最直接的办法是对每个可能的段求和,然后取最大值。这样的话,时间复杂度是 O(n2 )。最优的解是只扫描数组一遍,因此时间为 O(n)。假设 x1, x2, ..., xt 是最优解。那么,显然, 对任何 i <= t,x1, x2,..., xi 之和不可能为负。否则,砍去这一段,我们可以得到更大的值,这些该段的最优性矛盾。这就是说,最优解的段前缀不可能为负。而换句话说,如果一个段的和为负,则不可能是最优解的一部分。一开始,令当前段为从 x1 开始的段,置为空。我们从数组开始向前搜索,并把遇到的数加入当前段 s,同时记录目前遇到的最大和。这个过程一直持续到加入某个数 xi,使得 s 之和为负,则清空 s,然后以 xi 的下一个元素为当前段的开始,继续向前搜索。重复这个过程直到数组结束。在实现时,并不需要维护集合 s 并每次都对其对和,而只需要维护一个当前段的和,当有新元素加入当前段时,更新段的和;当重新开始一个段时,清 0 该段之和。
其它非最优算法见http://blog.csdn.net/pipisorry/article/details/39083281
/* O(n) 最优算法(记录左右边界) */ static int maxSubarraySum5(int *a, int n){ int sum = 0, max_sum = INT_MIN; int max_low = 0, max_high = 0; //最优子数组左右边界 int low = 0; //当前非<0前缀的子数组首下标 for(int i = 0; i < n; i++){ sum += a[i]; if(sum > max_sum){ max_sum = sum; max_high = i; max_low = low; } if(sum < 0){ //前缀<0时可以去掉sum的累积和 sum = 0; low = i + 1; } } printf("max_low = %d, max_high = %d\n", max_low, max_high); return max_sum; }
s = 0 max = 0 u = v = 1 // the starting index u and ending index v of current solution max_u = max_v = -1 // the starting and ending index of optimal solution for i from 1 to n s = s + xi if max < sum(s) then max = sum(s) v = i; max_v = v; max_u = u; end if if sum(s) < 0 then s = 0 // clear s u = v = i+1 end if end for return max, max_u, max_v
//***************************************************************************************/ //* 编程之美2.14 —— 求数组的子数组之和的最大值(微软亚研2006) 皮皮 2014-9-4 */ //***************************************************************************************/ #include <stdio.h> #include <assert.h> #include <malloc.h> #include <limits.h> int main(){ assert( freopen("BOP\\maxSubarraySum.in", "r", stdin) ); int cases; //测试案例数目 scanf("%d", &cases); while(cases--){ int n; //每个案例中数组元素个数 scanf("%d", &n); int *a = (int *)malloc(sizeof(int) * n); for(int i = 0; i < n; i++) scanf("%d", &a[i]); printf("%d\n\n", maxSubarraySum5(a, n)); } fclose(stdin); return 0; }
III 化归 -- 把问题一二转成问题三
看问题一。不难发现,问题一是问题三的二维版。由于一维的问题很好解,自然而然地,如果能把二维的降到一维的来处理,那么,事情就好办了。考虑子问题 OPT(i,j),其表示所有开始于第 i 行,结束于第 j 行的子矩阵中的最大和。在这些子矩阵中,起止行都一样,只是起止列不相同。也就是说,解 OPT(i,j),就只是找出某段列,使得其和最大。这就和第三个问题很相似了。为了化了问题三,我们把这些行都叠加到一起,变成一个单行,这就和问题三一样了:找出某个段,使得其和最大。然后,我们在所有 OPT(i,j) 中取最大值,即为原来问题的解。仔细地设计算法,可以使得其时间复杂度为 O(m2 n)。
/* 最优算法 AC 63MS */ static int maxSubmatrixSum(int **a, int n){ //初始化col_sum, col_sum[i][j][k]为第i和j行之间第k列元素的和 int *** col_sum; assert( col_sum = (int ***)malloc(sizeof(int **) * n) ); for(int i = 0; i < n; i++) assert( col_sum[i] = (int **)malloc(sizeof(int *) * n) ); for(int i = 0; i < n; i++){ for(int j = 0; j < n; j++){ assert( col_sum[i][j] = (int *)malloc(sizeof(int) * n) ); memset(col_sum[i][j], 0, sizeof(int) * n); } } //计算第0和j(>=1)行之间第k列的和 for(int k = 0; k < n; k++){ col_sum[0][0][k] = a[0][k]; //初始化col_sum[0][0][k]为首行数据 for(int j = 1; j < n; j++) col_sum[0][j][k] = col_sum[0][j - 1][k] + a[j][k]; //!!! } //计算第i和j行之间第k列的和 for(int k = 0; k < n; k++){ for(int i = 1; i < n; i++) for(int j = i; j < n; j++) col_sum[i][j][k] = col_sum[i - 1][j][k] - a[i - 1][k]; } //计算最大子矩阵和 int max_mat_sum = INT_MIN; for(int i = 0; i < n; i++){ for(int j = i; j < n; j++){ int row_sum = 0; //第i和j行之间最大行array和(压缩矩阵) for(int k = 0; k < n; k++){ row_sum += col_sum[i][j][k]; if(row_sum < 0) row_sum = 0; if(row_sum > max_mat_sum) max_mat_sum = row_sum; } } } return max_mat_sum; }
//***************************************************************************************/ //* 编程之美2.15 —— 求二维数组矩阵的元素之和最大子矩阵\poj 1050 皮皮 2014-9-4 */ //***************************************************************************************/ #include <stdio.h> #include <assert.h> #include <malloc.h> #include <limits.h> #include <string.h> int main(){ assert( freopen("BOP\\maxSubmatrixSum.in", "r", stdin) ); //int cases; //测试案例数目 //scanf("%d", &cases); //while(cases--){ int n; //每个案例中matrix维度 scanf("%d", &n); int **a = (int **)malloc(sizeof(int*) * n); for(int i = 0; i < n; i++) a[i] = (int *)malloc(sizeof(int) * n); for(int i = 0; i < n; i++) for(int j = 0; j < n; j++) scanf("%d", &a[i][j]); //printf("%d\n", maxSubmatrixSum1(a, n) ); //printf("%d\n", maxSubmatrixSum2(a, n) ); printf("%d\n", maxSubmatrixSum(a, n) ); /*}*/ fclose(stdin); return 0; }
对于问题二,类似的转化方法,只是在做行叠加时,如果都是 0 或 1,则为1,否则,为 -1。不再穆赘述。
IV 最大长方形(有问题?)
在直方图中,一个长方形由其左边界和右边界决定,其最大可能的高度由两者中的最小者决定。记 R(i,j) 为由第 i 个直方柱为左边界,第 j 个直方柱确定的面积最大的长方形。如果 R(i,j) 的面积最大,那么,第 i 个直方柱比第 i-1 个直方柱(如果存在的话)要高,而 第 j 个直方柱的高度也比第 j+1 个的要高,否则,由 R(i,j+1) 或 R(i-1,j) 的面积比 R(i,j) 还要大,这违背了 R(i,j) 的最优性。根据这个观察,我们从第1个直方柱开始,寻找第一个 i, 使得直方柱 i 的高度比 i+1 的大,则 i 是一个可能的右边界,而 i 之前的每一根直方柱都有可能是左边界(因为 i 是第一个比 i + 1 高的直方柱,所以,在 i 之前的是一个上升的直方柱序列,每一根都比前一根要高)。这时,我们计算前面所有可能的长方形的面积,并跟当前已知的最大值进行比较,并更新当前已知的最大值(如有必要的话)。然后,我们继续向前搜索第二个这样的 i 。重复这个过程,直到最后一根直方柱。这样,我们已经遍历了所有可能是最优解的长方形,并取了其中的最大值,因此,该算法是正确的。
至于时间复杂度,似乎还不太明显。对每个直方柱,我们通过跟后一个进行比较就知道其是否为一个可能的右边界。如果是,则需要对前面的一个上升序列的每个直方住计算其和 i 确定的最大长方形的面积,这个上升序列最差情况下似乎有 O(n) 长,时间复杂度最差似乎要 O(n2 )。其实不然,只要注意到,每个可能的左边界只会被计算一次,因此,总的时间复杂度为 O(n)。我们使用一个栈来保存前面的上升直方柱序列,当遇到一个可能的右边界时,把这些可能的左边界都弹出来,并计算其和右边界确定的长方形面积。显然,每个可能的左边界只会放栈一次。
V 化归 -- 把问题二转成问题四
依然是先考虑 1-聚类。从最后一行开始向上,某个列上的连续的 1 可以看做一个直方柱,直到碰到 0 或矩阵边界。而最大的1-聚类正是该“直方图”上的最大长方形。因此,我们可以用 OPT(i) 来表示终止于行 i 的最大的 1-聚类。这样,一共有 O(m) 个子问题,而每个子问题可以上面的方法解,时间复杂度为 O(n),因此总的时间复杂度为 O(mn)!不过,前提时,对每个子问题,我们可以只用 O(n) 的时间转换成一个“直方图”。事实上,除了最后一行开始,我们可以利用 OPT(i) 的直方图来构造 OPT(i-1) 的直方图,并且在整个过程中,每个元素只需要被计算一次即可。
这个解法相比上面的动态规划解要优美得多,而且时间复杂度更低!把一些比较难的或者维度比较高的问题化归到低维或经典的问题,往往可以得到意想不到的更好的解。
from:
http://blog.csdn.net/pipisorry/article/details/39048485
ref:
编程之美读书笔记2.15 - 子数组之和的最大值(二维)
http://blog.csdn.net/linulysses/article/details/5594141