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})
;m
和n
分别为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
:
- 状态定义:设动态规划矩阵
dp
,dp(i, j)
代表从棋盘的左上角开始,到达单元格(i, j)
时能拿到珠宝的最大累计价值。
- 转移方程:
-
- 当
i = 0
且j = 0
时,为起始元素 - 当
i = 0
且j != 0
时,为矩阵第一行元素,只可从左边到达 - 当
i != 0
且j = 0
时,为矩阵第一列元素,只可从上边到达 - 当
i != 0
且j != 0
时,可从左边或上边到达
- 当
- 初始状态:
dp[0][0] = frame[0][0]
,即到达单元格(0, 0)
时能拿到珠宝的最大累计价值为frame[0][0]
- 返回值:
dp[m−1][n−1]
,m
、n
分别为矩阵的行高和列宽,即返回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=0
或j=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)
:M
、N
分别为矩阵行高、列宽;动态规划需遍历整个frame
矩阵,使用O(MN)
时间 - 空间复杂度
O(1)
:原地修改使用常数大小的额外空间
此处为语雀内容卡片,点击链接查看:融码一生 · 语雀