刷题系列——某厂机试题目(一)【最大子矩阵】【动态规划】

题目描述

给定一个二维整数矩阵,要在这个矩阵中选出一个子矩阵,使得这个子矩阵内所有的数字和尽量大,我们把这个子矩阵称为和最大矩阵,子矩阵的选取原则是原矩阵中一块连续的矩形区域,单独一行、单独一列、整个矩阵,都算子矩阵。

解答要求:

  • 时间限制: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,ex1,ey),  P(sx,sy+1,ex,ey),  P(sx,sy,ex,ey1),  sum(sx,sy,ex,ey)}sx=exsy=eysx!=exsy!=ey

题解

第一版代码

上第一版代码。这版代码有优点也有缺点。优点很明显,易理解,基本上就是我们上面的思路转化成为的代码。缺点也很明显,

  1. 代码不够简洁,我们可以利用python语言的风格进行代码简化
  2. 重复子问题没有合并,因此性能低下
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在多次调用中的“独立”效果。

  • 2
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

angelavor

觉得有收获,给我个三连吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值