题目描述
给定一个二维整数矩阵,要在这个矩阵中选出一个子矩阵,使得这个子矩阵内所有的数字和尽量大,我们把这个子矩阵称为和最大矩阵,子矩阵的选取原则是原矩阵中一块连续的矩形区域,单独一行、单独一列、整个矩阵,都算子矩阵。
解答要求:
- 时间限制:C/C++ 1000ms,其他语言: 2000ms
- 内存限制:C/C++ 256MB,其他语言:512MB
输入:
输入的第一行包含2个整数n,m(1<=n,m<=10),表示一个n行m列矩阵,下面有n行,每行m个整数,同一行中,每两个数字之间一个空格,最后一个数字后没有空格,所有数字的取值范围为**[-1000,1000]**
输出:
输出一行,一个数字,表示选出的和最大子矩阵内所有数字的和。
样例:
# input
3 3
951 589 39
-583 -710 473
-229 501 -594
# output
1579
解释:
最大子矩阵就是
951 589 39
求和得到1579
解题思路
这道题一眼就可以看出是动态规划相关的题目,也是我个人认为是传统算法思想中比较难的一类算法题。解决动态规划问题的第一步是要找子问题,将原问题划分为多个子问题进行解决(这一步也往往是找迭代式的过程,只要能划分出子问题,那么迭代式自然就找到了)。
回顾一下分治算法与动态规划算法的区别,虽然这两个算法很相似,第一步都是要找子问题,但是不同点是分治算法子问题之间往往没有重叠(比如归并排序,只需要将子问题合并一下即可),然而动态规划子问题之间往往相互重叠(因为有重叠,所以效率低下,才需要动态规划,将相同子问题进行合并)。
寻找原问题的子问题
为了方便整理思路,我们以3×3矩阵为例。
考虑上面的矩阵我们应该如何划分子问题。一图胜千言,事实上,对于原矩阵的最大子矩阵和有可能存在于下面四个子矩阵中。
我们可以继续对划分后的子问题继续划分,直到一个子矩阵的上下左右都无法继续切掉时,划分就到头了。
考虑如何由子问题的解得到原问题的解
目前为止,我们知道了问题如何划分为子问题,以及子问题的边界。第二步要考虑如何运用子问题的解求解原问题的解。
首先要明白什么是问题的解。问题的解就是你要求的解,对于这个最大子矩阵问题,我们要找的问题的解就是一个矩阵的最大子矩阵中所有数的和。所以,我们每个划分的子问题都解决了一个子规模下与原问题同等的问题。 这句话看似废话,但却是理解如何从子问题构建原问题的解的关键。
对于最大子矩阵问题,我们只要四个子问题的解中最大的解,但是这个解不代表原问题的解,我们还要对当前原问题规模下的矩阵所有数进行加和,如果这个和比四个子问题中最大解还大,则原问题的解就是这个和;否则,原问题的解就是四个子问题中的最大解。稍后当我们列出迭代式就会更清晰。
考虑如何表示问题的迭代(写递推式)
因为题设中告诉我们必须是连续的矩阵,因此我们可以两个坐标点表示一个矩阵的规模,进而表示问题的解的定义。
设
s
x
,
s
y
,
e
x
,
e
y
sx,sy,ex,ey
sx,sy,ex,ey分别表示矩阵中的两个值的位置
(
s
x
,
s
y
)
(sx,sy)
(sx,sy),
(
e
x
,
e
y
)
(ex,ey)
(ex,ey),
P
(
s
x
,
s
y
,
e
x
,
e
y
)
P(sx,sy,ex,ey)
P(sx,sy,ex,ey)就表示在当前规模下问题的解,
M
a
t
r
i
x
[
i
]
[
j
]
Matrix[i][j]
Matrix[i][j]表示矩阵位置
(
i
,
j
)
(i,j)
(i,j)处的数字,
s
u
m
(
s
x
,
s
y
,
e
x
,
e
y
)
sum(sx,sy,ex,ey)
sum(sx,sy,ex,ey)表示对当前规模的矩阵中所有数进行求和。那么根据上面的思路,我们可以写成如下迭代式:
P
(
s
x
,
s
y
,
e
x
,
e
y
)
=
{
M
a
t
r
i
x
[
s
x
]
[
s
y
]
s
x
=
e
x
且
s
y
=
e
y
M
a
x
{
P
(
s
x
+
1
,
s
y
,
e
x
,
e
y
)
,
P
(
s
x
,
s
y
,
e
x
−
1
,
e
y
)
,
P
(
s
x
,
s
y
+
1
,
e
x
,
e
y
)
,
s
x
!
=
e
x
或
s
y
!
=
e
y
P
(
s
x
,
s
y
,
e
x
,
e
y
−
1
)
,
s
u
m
(
s
x
,
s
y
,
e
x
,
e
y
)
}
P(sx,sy,ex,ey) = \begin{cases} Matrix[sx][sy] & sx=ex 且 sy=ey \\ Max\large{\{} \\ \ \ P(sx+1,sy,ex,ey),\\ \ \ P(sx,sy,ex-1,ey),\\ \ \ P(sx,sy+1,ex,ey), & sx!=ex 或 sy!=ey\\ \ \ P(sx,sy,ex,ey-1),\\ \ \ sum(sx,sy,ex,ey) \\ \large{\}} \\ \end{cases}
P(sx,sy,ex,ey)=⎩⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎨⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎪⎧Matrix[sx][sy]Max{ P(sx+1,sy,ex,ey), P(sx,sy,ex−1,ey), P(sx,sy+1,ex,ey), P(sx,sy,ex,ey−1), sum(sx,sy,ex,ey)}sx=ex且sy=eysx!=ex或sy!=ey
题解
第一版代码
上第一版代码。这版代码有优点也有缺点。优点很明显,易理解,基本上就是我们上面的思路转化成为的代码。缺点也很明显,
- 代码不够简洁,我们可以利用python语言的风格进行代码简化
- 重复子问题没有合并,因此性能低下
def maxSubMatrix(matrix,*,pos):
sx,sy,ex,ey = pos
if sx==ex and sy==ey:
# 代表子矩阵中的最大子矩阵和
return matrix[sx][sy]
else:
# 可切上,可切下
res = [ 0 for i in range(5)]
if ex-sx>=1:
res[0] = maxSubMatrix(matrix,pos=(sx+1,sy,ex,ey)) # 切上
res[1] = maxSubMatrix(matrix,pos=(sx,sy,ex-1,ey)) # 切下
# 可切左,可切右
if ey-sy>=1:
res[2] = maxSubMatrix(matrix,pos=(sx,sy+1,ex,ey)) # 切左
res[3] = maxSubMatrix(matrix,pos=(sx,sy,ex,ey-1)) # 切右
for i in range(sx,ex+1):
for j in range(sy,ey+1):
res[4] += matrix[i][j]
maxres = max(res)
return maxres
第二版代码(代码更加简洁)
这版代码运用了较多python函数式的风格,我们只需要用一个复杂的表达式就可以表达我们的算法思想了。个人觉得,对python熟悉的人,这种写法在某种意义上讲更具备可读性。
def maxSubMatrix(matrix,*,pos):
sx,sy,ex,ey = pos
return matrix[sx][sy] if sx==ex and sy==ey else max(
maxSubMatrix(matrix,pos=(sx+1,sy,ex,ey)) if ex-sx>=1 else -2000,
maxSubMatrix(matrix,pos=(sx,sy,ex-1,ey)) if ex-sx>=1 else -2000,
maxSubMatrix(matrix,pos=(sx,sy+1,ex,ey)) if ey-sy>=1 else -2000,
maxSubMatrix(matrix,pos=(sx,sy,ex,ey-1)) if ey-sy>=1 else -2000,
sum([ tmatrix[i][j] for j in range(sy,ey+1) for i in range(sx,ex+1)])
)
第三版代码(优化性能)
在上面两版代码中,我们并没有对重复子问题进行合并,因此代码比较低效。在我自己机子上,规模到8×8之后,就有点跑不出来结果了。
首先需要理解什么是重复子问题。以本题为例,看图说话,一图胜千言。
我们可以看到上面2×3的矩阵和3×2的矩阵具有同样一个2×2的子问题,这就是重复子问题。因为对于特定的一组
(
s
x
,
s
y
,
e
x
,
e
y
)
(sx,sy,ex,ey)
(sx,sy,ex,ey),问题的解是一致的,因此重复的子问题我们仅需要递归计算一次即可。那如何做呢?思路很简单,如果我们有一个记录字典Dict
,字典的Key就是(sx,sy,ex,ey)
元组,Value就是这个规模下问题的解。那么当我们第二次遇到这个同样的子问题时,直接从字典中返回结果,不在继续递归即可。上第三版代码。
def cacheUp(key):
cache = dict()
def wrap(func):
def inner(*args,**kargs):
try:
cache[kargs[key]]
except:
cache[kargs[key]] = func(*args,**kargs)
return cache[kargs[key]]
return inner
return wrap
@cacheUp('pos')
def maxSubMatrix(matrix,*,pos):
sx,sy,ex,ey = pos
return matrix[sx][sy] if sx==ex and sy==ey else max(
maxSubMatrix(matrix,pos=(sx+1,sy,ex,ey)) if ex-sx>=1 else -2000,
maxSubMatrix(matrix,pos=(sx,sy,ex-1,ey)) if ex-sx>=1 else -2000,
maxSubMatrix(matrix,pos=(sx,sy+1,ex,ey)) if ey-sy>=1 else -2000,
maxSubMatrix(matrix,pos=(sx,sy,ex,ey-1)) if ey-sy>=1 else -2000,
sum([ tmatrix[i][j] for j in range(sy,ey+1) for i in range(sx,ex+1)])
)
这块代码主要增加了一个我自己设计的装饰器函数,对python装饰器不了解的读者可以先去补一下基础知识再来看会好一些。这个装饰器的含义就是为要装饰的函数(maxSubMatrix)加上一个“缓存”效果,当cache
中有对应Key的Value时,我们直接返回;没有的时候,先将问题的解存入“缓存”,然后再返回,这样下一次遇到这个子问题就可以直接从“缓存”中读取了。
这里需要注意的是,我为装饰器设计了一个参数pos
,这个参数需要和被装饰函数要缓存的关键字参数名对应起来。例如,这里,我们想要以maxSubMatrix
中的pos
参数为Key,对函数返回值进行缓存,那么必须给装饰器函数一个'pos'
参数。这样做的好处是即使我要被装饰的函数参数列表发生一些变化,我依然可以通过关键字参数对我要缓存的Key进行标注,实现了一个通用的缓存装饰器函数(只要是动态规划问题,需要合并重复子问题,都可以直接加一个这个装饰器即可实现性能优化)。
第四版代码(修复cache导致的一个小bug)
第三版代码看起来已经非常完美了,但是如果你尝试对两个相同规模不同数字的Matrix
调用两次maxSubMatrix
(甚至三次,四次),你会发现所有的结果都是和第一次调用结果保持一致。事实上,这是由装饰器中cache
变量导致的BUG,对于相同规模的Matrix
,pos=(sx,sy,ex,ey)是一样的,所以经过第一次调用,cache
记住了这个结果,因此到第二次,第三次之后因为pos一样,所以直接从cache
中返回了第一次的结果。
导致这个BUG的本质是由于函数闭包特性导致的,cache
变量在每次函数调用中并不独立,所以我们希望对于每次调用的时候cache
都能提前清零,这样对于每次调用都是一个独立的变量,就不会存在这个BUG了。上第四版代码。
def cacheUp(key):
cache = dict()
deep = 0
def wrap(func):
def inner(*args,**kargs):
nonlocal cache,deep
if deep==0:cache=dict()
try:
cache[kargs[key]]
except:
deep += 1
cache[kargs[key]] = func(*args,**kargs)
deep -= 1
return cache[kargs[key]]
return inner
return wrap
@cacheUp('pos')
def maxSubMatrix(matrix,*,pos):
sx,sy,ex,ey = pos
return matrix[sx][sy] if sx==ex and sy==ey else max(
maxSubMatrix(matrix,pos=(sx+1,sy,ex,ey)) if ex-sx>=1 else -2000,
maxSubMatrix(matrix,pos=(sx,sy,ex-1,ey)) if ex-sx>=1 else -2000,
maxSubMatrix(matrix,pos=(sx,sy+1,ex,ey)) if ey-sy>=1 else -2000,
maxSubMatrix(matrix,pos=(sx,sy,ex,ey-1)) if ey-sy>=1 else -2000,
sum([ tmatrix[i][j] for j in range(sy,ey+1) for i in range(sx,ex+1)])
)
我们引入一个deep
变量,并初始化为0,用来记录当前递归进行到了第几层。仅当deep==0
时,也即一开始调用时,我们才对cache
进行清零操作。这样就既保证了cache
在一次调用中的“缓存”效果,又保证了cache
在多次调用中的“独立”效果。