一、基本概念

    动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。

二、基本思想与策略

    基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

    由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。

    与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)

 


三、适用的情况

能采用动态规划求解的问题的一般要具有3个性质:

    (1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。

    (2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。

   (3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势

 


四、求解的基本步骤

     动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。

    初始状态→│决策1│→│决策2│→…→│决策n│→结束状态

                      图1 动态规划决策过程示意图

    (1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。

    (2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。

    (3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程

    (4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。

    一般,只要解决问题的阶段状态状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。

实际应用中可以按以下几个简化的步骤进行设计:

    (1)分析最优解的性质,并刻画其结构特征。

    (2)递归的定义最优解。

    (3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值

    (4)根据计算最优值时得到的信息,构造问题的最优解

 


五、算法实现的说明

    动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。

     使用动态规划求解问题,最重要的就是确定动态规划三要素

    (1)问题的阶段 (2)每个阶段的状态

    (3)从前一个阶段转化到后一个阶段之间的递推关系

     递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处

    确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。

          f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}

 


六、动态规划算法基本框架
复制代码
代码
 
    

数组连续最大和以及直方图最大矩

数组连续最大和以及直方图最大矩形问题(zz)  

2011-09-04 15:36:23|  分类:Algrithm |  标签:算法  数组连续元素最大和  直方图最大矩形  逆向思维  |举报|字号 订阅

直方图最大矩形问题来自公司面试题,这两个题目的解法都有类似之处。
解决问题的思路很巧妙,这几个问题都是有一个共同的思路,就是不考虑什么情况下会得出最优解,而是考虑最优解具有什么样的结构或者说特性,然后去寻找最优解。
寻找数组连续最大和问题和直方图最大矩形问题都是这样的思路,应该算是逆向思维,给人带来耳目一新的感觉啊。

在这篇文章里,我将探讨几个和求最大长方形相关的题目,并试图说明如何把一些相对复杂的问题化归成简单的易解的问题。这里的最大,可以指长方形内所有元素之各最大,也可以指面积最大。

问题一(最大和子矩阵) : 有一个 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 轴平行的长方形中,求该被该直方图包含的面积最大的长方形。(我猜测即使不要求长方形与 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,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 )。这种解法虽然可行,效率也还可以,但状态比较多,而且也不够优雅,状态的构造比较生硬。如果各位有更好的动态规划思路,还望不吝留言相告。

II 和最大的段问题

这个问题,最直接的办法是对每个可能的段求和,然后取最大值。这样的话,时间复杂度是 O(n2 )。最优的解是只扫描数组一遍,因此时间为 O(n)。假设 x1, x2, ..., xt 是最优解。那么,显然, 对任何 i <= t,x1, x2,..., xi 之和不可能为负。否则,砍去这一段,我们可以得到更大的值,这些该段的最优性矛盾。这就是说,最优解的段前缀不可能为负。而换句话说,如果一个段的和为负,则不可能是最优解的一部分。一开始,令当前段为从 x1 开始的段,置为空。我们从数组开始向前搜索,并把遇到的数加入当前段 s,同时记录目前遇到的最大和。这个过程一直持续到加入某个数 xi,使得 s 之和为负,则清空 s,然后以 xi 的下一个元素为当前段的开始,继续向前搜索。重复这个过程直到数组结束。在实现时,并不需要维护集合 s 并每次都对其对和,而只需要维护一个当前段的和,当有新元素加入当前段时,更新段的和;当重新开始一个段时,清 0 该段之和。

  1. s = 0  
  2. max = 0  
  3. u = v = 1 // the starting index u and ending index v of current solution  
  4. max_u = max_v = -1 // the starting and ending index of optimal solution  
  5. for i from 1 to n  
  6.     s = s + xi  
  7.     if max < sum(s) then  
  8.         max = sum(s)  
  9.         v = i;  
  10.         max_v = v;  
  11.         max_u = u;  
  12.     end if  
  13.     if sum(s) < 0 then   
  14.         s = 0 // clear s  
  15.         u = v = i+1  
  16.     end if  
  17. end for  
  18. return max, max_u, max_v  

III 化归 -- 把问题一二转成问题三

先看问题一。不难发现,问题一是问题三的二维版。由于一维的问题很好解,自然而然地,如果能把二维的降到一维的来处理,那么,事情就好办了。考虑子问题 OPT(i,j),其表示所有开始于第 i 行,结束于第 j 行的子矩阵中的最大和。在这些子矩阵中,起止行都一样,只是起止列不相同。也就是说,解 OPT(i,j),就只是找出某段列,使得其和最大。这就和第三个问题很相似了。为了化了问题三,我们把这些行都叠加到一起,变成一个单行,这就和问题三一样了:找出某个段,使得其和最大。然后,我们在所有 OPT(i,j) 中取最大值,即为原来问题的解。仔细地设计算法,可以使得其时间复杂度为 O(m2 n)。

  1. opt = 0  
  2. row_u, row_v, col_u, col_v // record the optimal solution  
  3. for i from 1 to m  
  4.     line = {0,0,...,0}  
  5.     for j from i to m  
  6.         for k from 1 to n  
  7.             line[k] = line[k] + x[j,k] // add row j to line  
  8.         end for  
  9.         (max,max_u,max_v) = solve maximum subarray problem one line[1..n]  
  10.         if opt < max then  
  11.             opt = max  
  12.             col_u = max_u  
  13.             col_v = max_v  
  14.             row_u = i;  
  15.             row_v = j;  
  16.     end for  
  17. end for  
  18. return opt, row_u, row_v, col_u, col_v  

对于问题二,类似的转化方法,只是在做行叠加时,如果都是 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)。我们使用一个栈来保存前面的上升直方柱序列,当遇到一个可能的右边界时,把这些可能的左边界都弹出来,并计算其和右边界确定的长方形面积。显然,每个可能的左边界只会放栈一次。

  1. max = 0  
  2. u = v = 0  
  3. for i from 1 to n+1  
  4.     h = i == n+1 ? 0 : bar[i].height  
  5.     if stack is empty, or h >= stack[top].height then  
  6.         push bar[i] into stack  
  7.     else  
  8.         repeat  
  9.             pop the top bar in stack to b  
  10.             area = b.height * (i-b.index)  
  11.             if max < area then  
  12.                 max = area  
  13.                 u = b.index  
  14.                 v = i-1  
  15.             end if  
  16.         until stack is empty or h > stack[top].height  
  17.     end if  
  18. end for  
  19. return max, u, v  

V 化归 -- 把问题二转成问题四

依然是先考虑 1-聚类。从最后一行开始向上,某个列上的连续的 1 可以看做一个直方柱,直到碰到 0 或矩阵边界。而最大的1-聚类正是该“直方图”上的最大长方形。因此,我们可以用 OPT(i) 来表示终止于行 i 的最大的 1-聚类。这样,一共有 O(m) 个子问题,而每个子问题可以上面的方法解,时间复杂度为 O(n),因此总的时间复杂度为 O(mn)!不过,前提时,对每个子问题,我们可以只用 O(n) 的时间转换成一个“直方图”。事实上,除了最后一行开始,我们可以利用 OPT(i) 的直方图来构造 OPT(i-1) 的直方图,并且在整个过程中,每个元素只需要被计算一次即可。

  1. opt = 0  
  2. row_u, row_v, col_u, col_v // the starting and ending of row and column of optimal solution, respectively  
  3. bar = [0,0,...,0]  
  4. for i from m to 1  
  5.     for j from 1 to n  
  6.         if i != m and x[i+1,j] = 1 then bar[j] = bar[j]-1  
  7.         else  
  8.             k = i  
  9.             repeat  
  10.                 bar[j] = bar[j]+1  
  11.                 k = k-1  
  12.             until k < 1 or x[k,j] = 0  
  13.         end if  
  14.     end for  
  15.     (max,u,v) = solve maximum rectangle in bar  
  16.     if opt < max then  
  17.         opt = max  
  18.         col_u = u  
  19.         col_v = v  
  20.         row_u = i - the height of the returned rectangle + 1  
  21.         row_v = i  
  22.     end if  
  23. end for  
  24. return opt, row_u, row_v, col_u, col_v  

这个解法相比上面的动态规划解要优美得多,而且时间复杂度更低!把一些比较难的或者维度比较高的问题化归到低维或经典的问题,往往可以得到意想不到的更好的解。先决条件是,积累足够多的题解,并善于观察题目与题目之间的联系和相似之处。

形问题(zz)  

2011-09-04 15:36:23|  分类:Algrithm |  标签:算法  数组连续元素最大和  直方图最大矩形  逆向思维  |举报|字号 订阅

直方图最大矩形问题来自公司面试题,这两个题目的解法都有类似之处。
解决问题的思路很巧妙,这几个问题都是有一个共同的思路,就是不考虑什么情况下会得出最优解,而是考虑最优解具有什么样的结构或者说特性,然后去寻找最优解。
寻找数组连续最大和问题和直方图最大矩形问题都是这样的思路,应该算是逆向思维,给人带来耳目一新的感觉啊。

在这篇文章里,我将探讨几个和求最大长方形相关的题目,并试图说明如何把一些相对复杂的问题化归成简单的易解的问题。这里的最大,可以指长方形内所有元素之各最大,也可以指面积最大。

问题一(最大和子矩阵) : 有一个 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 轴平行的长方形中,求该被该直方图包含的面积最大的长方形。(我猜测即使不要求长方形与 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,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 )。这种解法虽然可行,效率也还可以,但状态比较多,而且也不够优雅,状态的构造比较生硬。如果各位有更好的动态规划思路,还望不吝留言相告。

II 和最大的段问题

这个问题,最直接的办法是对每个可能的段求和,然后取最大值。这样的话,时间复杂度是 O(n2 )。最优的解是只扫描数组一遍,因此时间为 O(n)。假设 x1, x2, ..., xt 是最优解。那么,显然, 对任何 i <= t,x1, x2,..., xi 之和不可能为负。否则,砍去这一段,我们可以得到更大的值,这些该段的最优性矛盾。这就是说,最优解的段前缀不可能为负。而换句话说,如果一个段的和为负,则不可能是最优解的一部分。一开始,令当前段为从 x1 开始的段,置为空。我们从数组开始向前搜索,并把遇到的数加入当前段 s,同时记录目前遇到的最大和。这个过程一直持续到加入某个数 xi,使得 s 之和为负,则清空 s,然后以 xi 的下一个元素为当前段的开始,继续向前搜索。重复这个过程直到数组结束。在实现时,并不需要维护集合 s 并每次都对其对和,而只需要维护一个当前段的和,当有新元素加入当前段时,更新段的和;当重新开始一个段时,清 0 该段之和。

  1. s = 0  
  2. max = 0  
  3. u = v = 1 // the starting index u and ending index v of current solution  
  4. max_u = max_v = -1 // the starting and ending index of optimal solution  
  5. for i from 1 to n  
  6.     s = s + xi  
  7.     if max < sum(s) then  
  8.         max = sum(s)  
  9.         v = i;  
  10.         max_v = v;  
  11.         max_u = u;  
  12.     end if  
  13.     if sum(s) < 0 then   
  14.         s = 0 // clear s  
  15.         u = v = i+1  
  16.     end if  
  17. end for  
  18. return max, max_u, max_v  

III 化归 -- 把问题一二转成问题三

先看问题一。不难发现,问题一是问题三的二维版。由于一维的问题很好解,自然而然地,如果能把二维的降到一维的来处理,那么,事情就好办了。考虑子问题 OPT(i,j),其表示所有开始于第 i 行,结束于第 j 行的子矩阵中的最大和。在这些子矩阵中,起止行都一样,只是起止列不相同。也就是说,解 OPT(i,j),就只是找出某段列,使得其和最大。这就和第三个问题很相似了。为了化了问题三,我们把这些行都叠加到一起,变成一个单行,这就和问题三一样了:找出某个段,使得其和最大。然后,我们在所有 OPT(i,j) 中取最大值,即为原来问题的解。仔细地设计算法,可以使得其时间复杂度为 O(m2 n)。

  1. opt = 0  
  2. row_u, row_v, col_u, col_v // record the optimal solution  
  3. for i from 1 to m  
  4.     line = {0,0,...,0}  
  5.     for j from i to m  
  6.         for k from 1 to n  
  7.             line[k] = line[k] + x[j,k] // add row j to line  
  8.         end for  
  9.         (max,max_u,max_v) = solve maximum subarray problem one line[1..n]  
  10.         if opt < max then  
  11.             opt = max  
  12.             col_u = max_u  
  13.             col_v = max_v  
  14.             row_u = i;  
  15.             row_v = j;  
  16.     end for  
  17. end for  
  18. return opt, row_u, row_v, col_u, col_v  

对于问题二,类似的转化方法,只是在做行叠加时,如果都是 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)。我们使用一个栈来保存前面的上升直方柱序列,当遇到一个可能的右边界时,把这些可能的左边界都弹出来,并计算其和右边界确定的长方形面积。显然,每个可能的左边界只会放栈一次。

  1. max = 0  
  2. u = v = 0  
  3. for i from 1 to n+1  
  4.     h = i == n+1 ? 0 : bar[i].height  
  5.     if stack is empty, or h >= stack[top].height then  
  6.         push bar[i] into stack  
  7.     else  
  8.         repeat  
  9.             pop the top bar in stack to b  
  10.             area = b.height * (i-b.index)  
  11.             if max < area then  
  12.                 max = area  
  13.                 u = b.index  
  14.                 v = i-1  
  15.             end if  
  16.         until stack is empty or h > stack[top].height  
  17.     end if  
  18. end for  
  19. return max, u, v  

V 化归 -- 把问题二转成问题四

依然是先考虑 1-聚类。从最后一行开始向上,某个列上的连续的 1 可以看做一个直方柱,直到碰到 0 或矩阵边界。而最大的1-聚类正是该“直方图”上的最大长方形。因此,我们可以用 OPT(i) 来表示终止于行 i 的最大的 1-聚类。这样,一共有 O(m) 个子问题,而每个子问题可以上面的方法解,时间复杂度为 O(n),因此总的时间复杂度为 O(mn)!不过,前提时,对每个子问题,我们可以只用 O(n) 的时间转换成一个“直方图”。事实上,除了最后一行开始,我们可以利用 OPT(i) 的直方图来构造 OPT(i-1) 的直方图,并且在整个过程中,每个元素只需要被计算一次即可。

  1. opt = 0  
  2. row_u, row_v, col_u, col_v // the starting and ending of row and column of optimal solution, respectively  
  3. bar = [0,0,...,0]  
  4. for i from m to 1  
  5.     for j from 1 to n  
  6.         if i != m and x[i+1,j] = 1 then bar[j] = bar[j]-1  
  7.         else  
  8.             k = i  
  9.             repeat  
  10.                 bar[j] = bar[j]+1  
  11.                 k = k-1  
  12.             until k < 1 or x[k,j] = 0  
  13.         end if  
  14.     end for  
  15.     (max,u,v) = solve maximum rectangle in bar  
  16.     if opt < max then  
  17.         opt = max  
  18.         col_u = u  
  19.         col_v = v  
  20.         row_u = i - the height of the returned rectangle + 1  
  21.         row_v = i  
  22.     end if  
  23. end for  
  24. return opt, row_u, row_v, col_u, col_v  

这个解法相比上面的动态规划解要优美得多,而且时间复杂度更低!把一些比较难的或者维度比较高的问题化归到低维或经典的问题,往往可以得到意想不到的更好的解。先决条件是,积累足够多的题解,并善于观察题目与题目之间的联系和相似之处。