最大子序列和与最大矩阵

 阅读约 8 分钟

最大子矩阵

<题目来源:百度2017秋招,http://exercise.acmcoder.com/... >

问题描述

给出一个n行m列的二维矩阵A[m,n],其每个元素的取值范围是[-1000,1000],其中1<=n<=100,1<=m<=100。求出p,q,r,s,满足条件1<=p<=q<=n,1<=r<=s<=m且p<=i<=q,r<=j<=s的(i,j)对应的A[i,j]之和最大。

在解决这个问题之前我们先来看另外一个问题:

给定一整数序列A1, A2,... An (可能有负数),求A1~An的一个子序列Ai~Aj,使得Ai到Aj的和最大,需要注意到i...j是连续的 。例如:整数序列-2, 11, -4, 13, -5, 2, -5, -3, 12, -9的最大子序列的和为21。

这个问题出现在很多公司的笔试以及作为算法竞赛入门的一个经典题目,有很多种方法,最朴素的思想是用一个3重循环来解决,复杂度O(n^3)。如果注意到在最内层的循环做了很多重复计算的时候,我们可以预处理计算和,用sum[i]表示从第一个元素到第i个元素的和(其中sum[0] = 0),当需要求i...j之间的和时,我们可以用sum[j] - sum[i - 1]得到。复杂度变成O(n^2)

有没有更快的方法?

这里为了引出第一个大问题的解决方法,采用了动态规划(动态规划是运筹学的一个分支,包含的模型非常多,这里对只这个问题作分析)

首先考虑,这个是一个线性的问题,对于任何一个数ai而言,它可能是与前面的若干个元素构成一个子序列,也可以是以自己开头,构成一个新的序列,这取决于a[i]与已经获得的最大子序列的和的关系,设f[i]表示以i元素结尾的子序列可以获得的最大和,那么
如果f[i-1] + a[i] > a[i],那么我们将a[i]作为当前序列的结尾,否者以a[i]作为一个新序列的开头,原因是如果a[i]的加入使得原来的最大和变小了,那么无论后面怎么选择,都不可能比a[i]作为一个新的序列开头可以得到更大的和。

对上面的式子变形条件变为f[i-1] > 0,这样更直观,如果前面的序列为负数或者0,连接a[i]甚至后面更多的元素构成的子序列都不可能使得这个序列变得更大,都不如从a[i]开始的子序列。因此,这个地方我们需要断开这个子序列。

最后,我们需要考虑的是在整个序列的哪个位置结尾可以获得最大的值?因为后面如果包含负数就让这个序列的和变小了。那么我们只需在计算f[i]的时候记录一个最大的f[i]就是问题的答案
f[i] = max{f[i - 1] + a[i], a[i]} (1 <= i <= n, 其中f[i] = 0)
至此,这个问题得到了解决,时间复杂度进一步降低到O(n),考虑到数据的读入最少需要O(n)的时间,已经完全可以接受了。

*更多的解题方法可以参考:
http://blog.csdn.net/hcbbt/ar...*

回到我们要解决的最大子矩阵问题,我们发现在某些情况下我们要求解的最大子矩阵和最大子序列是同样的问题,就是在当矩形的高(也就是行数)为1的时候

例如 n = 1, m = 7

3 1 -5 7 3 5 -2

显然,用最大序列和问题的解决思路就可以处理了,得到一个1 * 3的矩形7 3 5
当问题扩展为多行之后,似乎这个办法就不管用了,因为每层求一个最大最后的结果可能不是一个矩形。我们希望能继续运用这个思想在处理这个问题上,那么我们考虑要对这个矩形进行一个转化,一个降维处理。我们来看一种情况:

例如 n = 3, m = 7
3 1 -5 7 3 5 -2
1 -2 3 5 -1 2 4
2 2 1 -3 2 5 3

如果对于这个矩阵而言,我们求出子矩阵的高h有多少种可能?实际上只有3种可能,h = 1,2,3
如果是1,子矩阵可以出现在n = 1, 2, 3的任何一行,如果h = 2,那么可以是子矩阵的高上下边界分别在1 2,也可以是2 3,如果h = 3,那么只有一种可能,就是子矩阵的高上下边分别位于1和3。问题在这里变得比较有趣了,子矩阵的上下边必然是这个矩形中的某一行或者两行,既然决定不了它在哪里可以使得子矩阵最大,那么我们选择枚举这个子矩阵的高的上下界。枚举后出现了一个怎样的情况?

要处理的目标变成
3 1 -5 7 3 5 -2
or
1 -2 3 5 -1 2 4
or
2 2 1 -3 2 5 3
or
3 1 -5 7 3 5 -2
1 -2 3 5 -1 2 4
and so on...

这个时候,由于高已经固定了(并且枚举了所有的情况),我们要考虑的仅仅是如何在其中选择一个连续的部分来组成子矩阵即可。进一步,如果我们将这些枚举的固定高的子矩阵进一步压缩,也就是通一列的所有元素相加,最后就变成了我们刚才已经解决的最大子序列和的问题
例如,上面例子中高为2的子矩阵压缩后变成

4 -1 -2 12 2 7 2

我们来估计下时间复杂度:
首先我们需要一个n*n的时间来枚举子矩阵的高的两边所在的行,然后再需要一个n的时间来计算一个最大序列和问题。总计时间复杂度是O(n^3)

需要注意到我们是如何实现在O(n)内计算出最大子序列和的,因为要实现压缩操作。实际上的情况是这部分内容我们需要预处理:

设sum[i, j]表示第i列,从第1行到第j行的和,那么sum[i, j] = sum[i, j - 1] + matrix[j, i] (主要行列的关系,matrix是原始的矩阵信息)
当我们需要压缩第col列i, j(i <= j)行之间的这些值时

val = sum[col, j] - sum[col, i - 1]

这些处理需要一个O(n^2)的时间复杂度,但是这个操作在预处理时进行的,而不是嵌套的,因此,解决该问题的时间复杂仍然是O(n^3)

此外,还有一些衍生问题,比如最大m子段和,这个部分网上仍然有很多资料,有兴趣可以尝试解决它。

import sys

const_max_num = 100


def main():
    matrix = [[0 for i in range(const_max_num + 1)]for i in range(const_max_num + 1)]
    t_case = int(raw_input())

    for t in range(t_case):
        temp = raw_input().split(' ')
        n = int(temp[0])
        m = int(temp[1])

        for i in range(1, n + 1):
            line = raw_input().split(' ')
            for j in range(1, m + 1):
                matrix[i][j] = int(line[j - 1])

        col_sum = [[0 for i in range(const_max_num + 1)]for i in range(const_max_num + 1)]
        for col in range(1, m + 1):
            for row in range(1, n + 1):
                col_sum[col][row] = col_sum[col][row - 1] + matrix[row][col]

        line = []
        max_area = -sys.maxint
        for row_start in range(1, n + 1):
            for row_end in range(row_start, n + 1):
                for i in range(1, m + 1):
                    line.append(col_sum[i][row_end] - col_sum[i][row_start - 1])
                cur = 0
                for l in line:
                    cur = max(cur + l, l)
                    max_area = max(max_area, cur)
                line[:] = []

        print max_area



if __name__ == '__main__':
    main()
阅读 2.1k更新于 2017-04-14

推荐阅读
Lite's home
用户专栏

和大家分享一些经典的算法,以及解决问题的思路和方法

1 人关注
12 篇文章
专栏主页
目录