166_珠宝的最高价值【M】

1、递归 + 缓存

(1)方式 1

class Solution:
    def jewelleryValue(self, frame: List[List[int]]) -> int:
        # jy: 如果去除缓存逻辑, 则会导致超时
        dict_ = {}
        def dfs(arr, row, col):
            """
            计算从 arr[row][col] 走到右下角的最大价值
            """
            if (row, col) in dict_:
                return dict_[(row, col)]
                
            if not arr:
                return 0

            row_num, col_num = len(arr), len(arr[0])
            if row >= row_num or col >= col_num:
                return 0

            res_ = arr[row][col] + max(dfs(arr, row+1, col),
                                       dfs(arr, row, col+1))
            dict_[(row, col)] = res_
            return res_

        return dfs(frame, 0, 0)

(2)方式 2

  • 无缓存版本的复杂度分析:
    • 时间复杂度:O(2^{m+n})mn分别为grid的行数和列数。搜索树可以近似为一棵二叉树,树高为O(m+n),即从grid左上角到右下角经过的格子数,所以节点个数为O(2^{m+n})
    • 空间复杂度:O(m+n);递归需要O(m+n)的栈空间
  • 有缓存版本的复杂度分析:
    • 时间复杂度:O(mn);由于每个状态只会计算一次,因此动态规划的时间复杂度 = 状态个数 × 单个状态的计算时间;本题的状态个数为O(mn),单个状态的计算时间为O(1),因此时间复杂度为O(mn)
    • 空间复杂度:O(mn)
# jy: 无缓存版本 (超时)
class Solution:
    def jewelleryValue(self, grid: List[List[int]]) -> int:
        def dfs(i: int, j: int) -> int:
            """
            求从左上角走到 grid[i][j] 的最大价值
            """
            if i < 0 or j < 0:
                return 0
            return max(dfs(i, j-1), dfs(i-1, j)) + grid[i][j]
        return dfs(len(grid) - 1, len(grid[0]) - 1)


# jy: 有缓存版本 (不超时)
class Solution:
    def jewelleryValue(self, grid: List[List[int]]) -> int:
        dict_ = {}
        def dfs(i: int, j: int) -> int:
            """
            求从左上角走到 grid[i][j] 的最大价值
            """
            if i < 0 or j < 0:
                return 0

            if (i, j) in dict_:
                return dict_[(i, j)]

            res_ = max(dfs(i, j-1), dfs(i-1, j)) + grid[i][j]
            dict_[(i, j)] = res_
            return res_
        return dfs(len(grid) - 1, len(grid[0]) - 1)

2、动态规划(性能极佳)

  • 根据题目说明,易得某单元格只可能从上边单元格或左边单元格到达。
  • f(i, j)为从棋盘左上角走至单元格(i, j)的珠宝最大累计价值,易得到以下递推关系:f(i, j)等于f(i, j−1)f(i−1, j)中的较大值加上当前单元格珠宝价值frame(i,j)
    • f(i, j) = max[f(i, j−1), f(i−1, j)] + frame(i, j)
  • 因此,可用动态规划解决此问题,以上公式便为转移方程。
  • 下图中的grid对应本题的frame

  • 状态定义:设动态规划矩阵dpdp(i, j)代表从棋盘的左上角开始,到达单元格(i, j)时能拿到珠宝的最大累计价值。
  • 转移方程:
    • i = 0j = 0时,为起始元素
    • i = 0j != 0时,为矩阵第一行元素,只可从左边到达
    • i != 0j = 0时,为矩阵第一列元素,只可从上边到达
    • i != 0j != 0时,可从左边或上边到达

  • 初始状态:dp[0][0] = frame[0][0],即到达单元格(0, 0)时能拿到珠宝的最大累计价值为frame[0][0]
  • 返回值:dp[m−1][n−1]mn分别为矩阵的行高和列宽,即返回dp矩阵右下角元素
  • 空间优化:由于dp[i][j]只与dp[i−1][j]dp[i][j−1]frame[i][j]有关系,因此可以将原矩阵frame用作dp矩阵,即直接在frame上修改即可;省去dp矩阵使用的额外空间,因此空间复杂度从O(MN)降至O(1)

class Solution:
    def jewelleryValue(self, frame: List[List[int]]) -> int:
        for i in range(len(frame)):
            for j in range(len(frame[0])):
                if i == 0 and j == 0:
                    continue
                if i == 0: 
                    frame[i][j] += frame[i][j-1]
                elif j == 0: 
                    frame[i][j] += frame[i-1][j]
                else: 
                    frame[i][j] += max(frame[i][j-1], frame[i-1][j])
        return frame[-1][-1]

  • 以上代码逻辑仍可提升效率:当frame矩阵很大时,i=0j=0的情况仅占极少数,而以上代码逻辑中每轮循环都冗余了一次判断。因此,可先初始化矩阵第一行和第一列,再开始遍历递推:
class Solution:
    def jewelleryValue(self, frame: List[List[int]]) -> int:
        m, n = len(frame), len(frame[0])
        
        # jy: 初始化第一行
        for j in range(1, n): 
            frame[0][j] += frame[0][j-1]
            
        # jy: 初始化第一列
        for i in range(1, m): 
            frame[i][0] += frame[i-1][0]
            
        for i in range(1, m):
            for j in range(1, n):
                frame[i][j] += max(frame[i][j-1], frame[i-1][j])
        return frame[-1][-1]
  • 时间复杂度O(MN)MN分别为矩阵行高、列宽;动态规划需遍历整个frame矩阵,使用O(MN)时间
  • 空间复杂度O(1):原地修改使用常数大小的额外空间

此处为语雀内容卡片,点击链接查看:融码一生 · 语雀

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

融码一生

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值