跳至主要內容

02. 线性 DP 知识(二)

ITCharge大约 9 分钟

线性 DP 知识(二)

4. 矩阵线性 DP问题

矩阵线性 DP 问题:问题的输入为二维矩阵的线性 DP 问题。状态一般可定义为 dp[i][j]dp[i][j],表示为:从「位置 (0,0)(0, 0)」到达「位置 (i,j)(i, j)」的相关解。

4.1 最小路径和

4.1.1 题目链接

4.1.2 题目大意

描述:给定一个包含非负整数的 m×nm \times n 大小的网格 gridgrid

要求:找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明

  • 每次只能向下或者向右移动一步。
  • m==grid.lengthm == grid.length
  • n==grid[i].lengthn == grid[i].length
  • 1m,n2001 \le m, n \le 200
  • 0grid[i][j]1000 \le grid[i][j] \le 100

示例

  • 示例 1:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 13111 的总和最小。
  • 示例 2:
输入:grid = [[1,2,3],[4,5,6]]
输出:12

4.1.3 解题思路

思路 1:动态规划
1. 划分阶段

按照路径的结尾位置(行位置、列位置组成的二维坐标)进行阶段划分。

2. 定义状态

定义状态 dp[i][j]dp[i][j] 为:从位置 (0,0)(0, 0) 到达位置 (i,j)(i, j) 的最小路径和。

3. 状态转移方程

当前位置 (i,j)(i, j) 只能从左侧位置 (i,j1)(i, j - 1) 或者上方位置 (i1,j)(i - 1, j) 到达。为了使得从左上角到达 (i,j)(i, j) 位置的最小路径和最小,应从 (i,j1)(i, j - 1) 位置和 (i1,j)(i - 1, j) 位置选择路径和最小的位置达到 (i,j)(i, j)

即状态转移方程为:dp[i][j]=min(dp[i][j1],dp[i1][j])+grid[i][j]dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]

4. 初始条件
  • 当左侧和上方是矩阵边界时(即 i=0,j=0i = 0, j = 0),dp[i][j]=grid[i][j]dp[i][j] = grid[i][j]
  • 当只有左侧是矩阵边界时(即 i0,j=0i \ne 0, j = 0),只能从上方到达,dp[i][j]=dp[i1][j]+grid[i][j]dp[i][j] = dp[i - 1][j] + grid[i][j]
  • 当只有上方是矩阵边界时(即 i=0,j0i = 0, j \ne 0),只能从左侧到达,dp[i][j]=dp[i][j1]+grid[i][j]dp[i][j] = dp[i][j - 1] + grid[i][j]
5. 最终结果

根据状态定义,最后输出 dp[rows1][cols1]dp[rows - 1][cols - 1](即从左上角到达 (rows1,cols1)(rows - 1, cols - 1) 位置的最小路径和)即可。其中 rowsrowscolscols 分别为 gridgrid 的行数、列数。

思路 1:代码
class Solution:
    def minPathSum(self, grid: List[List[int]]) -> int:
        rows, cols = len(grid), len(grid[0])
        dp = [[0 for _ in range(cols)] for _ in range(rows)]

        dp[0][0] = grid[0][0]
        
        for i in range(1, rows):
            dp[i][0] = dp[i - 1][0] + grid[i][0]
        
        for j in range(1, cols):
            dp[0][j] = dp[0][j - 1] + grid[0][j]

        for i in range(1, rows):
            for j in range(1, cols):
                dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]
            
        return dp[rows - 1][cols - 1]
思路 1:复杂度分析
  • 时间复杂度O(mn)O(m * n),其中 mmnn 分别为 gridgrid 的行数和列数。
  • 空间复杂度O(mn)O(m * n)

4.2 最大正方形

4.2.1 题目链接

4.2.2 题目大意

描述:给定一个由 '0''1' 组成的二维矩阵 matrixmatrix

要求:找到只包含 '1' 的最大正方形,并返回其面积。

说明

  • m==matrix.lengthm == matrix.length
  • n==matrix[i].lengthn == matrix[i].length
  • 1m,n3001 \le m, n \le 300
  • matrix[i][j]matrix[i][j]'0''1'

示例

  • 示例 1:
输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:4
  • 示例 2:
输入:matrix = [["0","1"],["1","0"]]
输出:1

4.2.3 解题思路

思路 1:动态规划
1. 划分阶段

按照正方形的右下角坐标进行阶段划分。

2. 定义状态

定义状态 dp[i][j]dp[i][j] 表示为:以矩阵位置 (i,j)(i, j) 为右下角,且值包含 11 的正方形的最大边长。

3. 状态转移方程

只有当矩阵位置 (i,j)(i, j) 值为 11 时,才有可能存在正方形。

  • 如果矩阵位置 (i,j)(i, j) 上值为 00,则 dp[i][j]=0dp[i][j] = 0
  • 如果矩阵位置 (i,j)(i, j) 上值为 11,则 dp[i][j]dp[i][j] 的值由该位置上方、左侧、左上方三者共同约束的,为三者中最小值加 11。即:dp[i][j]=min(dp[i1][j1],dp[i1][j],dp[i][j1])+1dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1
4. 初始条件
  • 默认所有以矩阵位置 (i,j)(i, j) 为右下角,且值包含 11 的正方形的最大边长都为 00,即 dp[i][j]=0dp[i][j] = 0
5. 最终结果

根据我们之前定义的状态, dp[i][j]dp[i][j] 表示为:以矩阵位置 (i,j)(i, j) 为右下角,且值包含 11 的正方形的最大边长。则最终结果为所有 dp[i][j]dp[i][j] 中的最大值。

思路 1:代码
class Solution:
    def maximalSquare(self, matrix: List[List[str]]) -> int:
        rows, cols = len(matrix), len(matrix[0])
        max_size = 0
        dp = [[0 for _ in range(cols + 1)] for _ in range(rows + 1)]
        for i in range(rows):
            for j in range(cols):
                if matrix[i][j] == '1':
                    if i == 0 or j == 0:
                        dp[i][j] = 1
                    else:
                        dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1
                    max_size = max(max_size, dp[i][j])
        return max_size * max_size
思路 1:复杂度分析
  • 时间复杂度O(m×n)O(m \times n),其中 mmnn 分别为二维矩阵 matrixmatrix 的行数和列数。
  • 空间复杂度O(m×n)O(m \times n)

5. 无串线性 DP 问题

无串线性 DP 问题:问题的输入不是显式的数组或字符串,但依然可分解为若干子问题的线性 DP 问题。

5.1 整数拆分

5.1.1 题目链接

5.1.2 题目大意

描述:给定一个正整数 nn,将其拆分为 k(k2)k (k \ge 2) 个正整数的和,并使这些整数的乘积最大化。

要求:返回可以获得的最大乘积。

说明

  • 2n582 \le n \le 58

示例

  • 示例 1:
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1
  • 示例 2:
输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36

5.1.3 解题思路

思路 1:动态规划
1. 划分阶段

按照正整数进行划分。

2. 定义状态

定义状态 dp[i]dp[i] 表示为:将正整数 ii 拆分为至少 22 个正整数的和之后,这些正整数的最大乘积。

3. 状态转移方程

i2i \ge 2 时,假设正整数 ii 拆分出的第 11 个正整数是 j(1j<i)j(1 \le j < i),则有两种方法:

  1. ii 拆分为 jjiji - j 的和,且 iji - j 不再拆分为多个正整数,此时乘积为:j×(ij)j \times (i - j)
  2. ii 拆分为 jjiji - j 的和,且 iji - j 继续拆分为多个正整数,此时乘积为:j×dp[ij]j \times dp[i - j]

dp[i]dp[i] 取两者中的最大值。即:dp[i]=max(j×(ij),j×dp[ij])dp[i] = max(j \times (i - j), j \times dp[i - j])

由于 1j<i1 \le j < i,需要遍历 jj 得到 dp[i]dp[i] 的最大值,则状态转移方程如下:

dp[i]=max1j<i{max(j×(ij),j×dp[ij])}dp[i] = max_{1 \le j < i}\lbrace max(j \times (i - j), j \times dp[i - j]) \rbrace

4. 初始条件
  • 0011 都不能被拆分,所以 dp[0]=0,dp[1]=0dp[0] = 0, dp[1] = 0
5. 最终结果

根据我们之前定义的状态,dp[i]dp[i] 表示为:将正整数 ii 拆分为至少 22 个正整数的和之后,这些正整数的最大乘积。则最终结果为 dp[n]dp[n]

思路 1:代码
class Solution:
    def integerBreak(self, n: int) -> int:
        dp = [0 for _ in range(n + 1)]
        for i in range(2, n + 1):
            for j in range(i):
                dp[i] = max(dp[i], (i - j) * j, dp[i - j] * j)
        return dp[n]
思路 1:复杂度分析
  • 时间复杂度O(n2)O(n^2)
  • 空间复杂度O(n)O(n)

5.2 只有两个键的键盘

5.2.1 题目链接

5.2.2 题目大意

描述:最初记事本上只有一个字符 'A'。你每次可以对这个记事本进行两种操作:

  • Copy All(复制全部):复制这个记事本中的所有字符(不允许仅复制部分字符)。
  • Paste(粘贴):粘贴上一次复制的字符。

现在,给定一个数字 nn,需要使用最少的操作次数,在记事本上输出恰好 nn'A'

要求:返回能够打印出 nn'A' 的最少操作次数。

说明

  • 1n10001 \le n \le 1000

示例

  • 示例 1:
输入:3
输出:3
解释
最初, 只有一个字符 'A'。
第 1, 使用 Copy All 操作。
第 2, 使用 Paste 操作来获得 'AA'。
第 3, 使用 Paste 操作来获得 'AAA'
  • 示例 2:
输入:n = 1
输出:0

5.2.3 解题思路

思路 1:动态规划
1. 划分阶段

按照字符 'A' 的个数进行阶段划分。

2. 定义状态

定义状态 dp[i]dp[i] 表示为:通过「复制」和「粘贴」操作,得到 ii 个字符 'A',最少需要的操作数。

3. 状态转移方程
  1. 对于 ii 个字符 'A',如果 ii 可以被一个小于 ii 的整数 jj 除尽(jjii 的因子),则说明 jj 个字符 'A' 可以通过「复制」+「粘贴」总共 ij\frac{i}{j} 次得到 ii 个字符 'A'
  2. 而得到 jj 个字符 'A',最少需要的操作数可以通过 dp[j]dp[j] 获取。

则我们可以枚举 ii 的因子,从中找到在满足 jj 能够整除 ii 的条件下,最小的 dp[j]+ijdp[j] + \frac{i}{j},即为 dp[i]dp[i],即 dp[i]=minji(dp[i],dp[j]+ij)dp[i] = min_{j | i}(dp[i], dp[j] + \frac{i}{j})

由于 jj 能够整除 ii,则 jjij\frac{i}{j} 都是 ii 的因子,两者中必有一个因子是小于等于 i\sqrt{i} 的,所以在枚举 ii 的因子时,我们只需要枚举区间 [1,i][1, \sqrt{i}] 即可。

综上所述,状态转移方程为:dp[i]=minji(dp[i],dp[j]+ij,dp[ij]+j)dp[i] = min_{j | i}(dp[i], dp[j] + \frac{i}{j}, dp[\frac{i}{j}] + j)

4. 初始条件
  • i=1i = 1 时,最少需要的操作数为 00。所以 dp[1]=0dp[1] = 0
5. 最终结果

根据我们之前定义的状态,dp[i]dp[i] 表示为:通过「复制」和「粘贴」操作,得到 ii 个字符 'A',最少需要的操作数。 所以最终结果为 dp[n]dp[n]

思路 1:动态规划代码
import math

class Solution:
    def minSteps(self, n: int) -> int:
        dp = [0 for _ in range(n + 1)]
        for i in range(2, n + 1):
            dp[i] = float('inf')
            for j in range(1, int(math.sqrt(n)) + 1):
                if i % j == 0:
                    dp[i] = min(dp[i], dp[j] + i // j, dp[i // j] + j)

        return dp[n]
思路 1:复杂度分析
  • 时间复杂度O(nn)O(n \sqrt{n})。外层循环遍历的时间复杂度是 O(n)O(n),内层循环遍历的时间复杂度是 O(n)O(\sqrt{n}),所以总体时间复杂度为 O(nn)O(n \sqrt{n})
  • 空间复杂度O(n)O(n)。用到了一维数组保存状态,所以总体空间复杂度为 O(n)O(n)

参考资料