目录
[509. 斐波那契数](#509. 斐波那契数)
[70. 爬楼梯](#70. 爬楼梯)
[746. 使用最小花费爬楼梯](#746. 使用最小花费爬楼梯)
[62. 不同路径](#62. 不同路径)
[63. 不同路径 II](#63. 不同路径 II)
[343. 整数拆分](#343. 整数拆分)
[96. 不同的二叉搜索树](#96. 不同的二叉搜索树)
[46. 携带研究材料(0-1背包问题)](#46. 携带研究材料(0-1背包问题))
[使用一维dp数组做状态压缩 :](#使用一维dp数组做状态压缩 :)
分割等和子集
1. dp数组以及下标的含义
2. 状态转移方程
3. dp数组初始化
4. 确定遍历顺序
5. 校验dp变化过程
509. 斐波那契数
1. dp数组以及下标的含义:
只需要维护三个数,a, b 对应做加法的两个数,c 作为中间变量记录本轮的加和结果
2. 状态转移方程
python
c = a + b
a, b = b, c
3. dp数组初始化
对n == 0 以及 n <= 2 这三组已知的初始化条件进行赋值即可
4. 确定遍历顺序
略
5. 校验dp变化过程:
略
实现:
python
class Solution:
def fib(self, n: int) -> int:
if n == 0:
return 0
if n <= 2:
return 1
a = b = 1
for i in range(2, n):
c = a + b
a, b = b, c
return c
70. 爬楼梯
1. dp数组以及下标的含义:
(n + 1) 长度的数组,表示爬到第n阶台阶总共有多少种方法
2. 状态转移方程
dp[i] = dp[i - 1] + dp[i - 2] 最后一步固定的情况下,计算能够一步到达的位置有多少种方式抵达即可
3. dp数组初始化
dp[0] = 0, dp[1] = 1, dp[2] = 2
4. 确定遍历顺序
顺序遍历即可
5. 校验dp变化过程:
略
实现:
python
class Solution:
def climbStairs(self, n: int) -> int:
if n == 1:
return 1
dp = [0] * (n + 1)
dp[0] = 0
dp[1] = 1
dp[2] = 2
for i in range(3, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
746. 使用最小花费爬楼梯
1. dp数组以及下标的含义:
一维dp数组,代表爬到指定位置 i 需要花费的最少体力。
2. 状态转移方程
和上一题类似,爬到指定某个位置的方案主要来自前面的子问题。衡量最少体力则需要比较跨一级台阶和跨两级台阶加上各自之前花费的体力的总和哪个更小。因此状态转移方程为:
python
dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
3. dp数组初始化
对dp使用全0初始化即可,因为楼梯级数是从2开始的。
4. 确定遍历顺序
顺序遍历
5. 校验dp变化过程:
略
实现:
python
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
dp = [0] * (len(cost) + 1)
for i in range(2, len(cost) + 1):
dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
return dp[-1]
62. 不同路径
1. dp数组以及下标的含义
二维dp数组,每个格点表示到达这里有多少条不同的路径
2. 状态转移方程
中间某个格点的路径总数由它上方和左方的路径数相加得到(二维爬楼梯)
3. dp数组初始化
使用全1初始化,不需要引入边界长度,直接从第二行开始计算即可
4. 确定遍历顺序
顺序遍历,先遍历行或者列都可以
5. 校验dp变化过程
[[1, 1, 1, 1, 1, 1, 1], [1, 2, 3, 4, 5, 6, 7], [1, 3, 6, 10, 15, 21, 28]]
实现:
python
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[1] * n for _ in range(m)]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
print(dp)
return dp[-1][-1]
63. 不同路径 II
1. dp数组以及下标的含义:
和上一题的含义一样,也是路径数。
2. 状态转移方程
遇到障碍则将这个障碍格点的值处理成0
3. dp数组初始化
全0初始化,对第一行和第一列进行额外初始化,在障碍前的格点全部初始化成1
4. 确定遍历顺序
顺序遍历,行或列都可以,但注意不要搞错嵌套的变量名
5. 校验dp变化过程
obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
初始化:[[1, 1, 1], [1, 0, 0], [1, 0, 0]]
最终dp:[[1, 1, 1], [1, 0, 1], [1, 1, 2]]
实现:
python
class Solution:
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
## 排除第一个格点就是障碍的边界
if obstacleGrid[0][0] == 1:
return 0
m = len(obstacleGrid)
n = len(obstacleGrid[0])
## 构建dp数组 不需要额外的边界
dp = [[0] * n for _ in range(m)]
## 对首行和首列做初始化
for i in range(n):
if obstacleGrid[0][i] != 1:
dp[0][i] = 1
else:
break
for i in range(m):
if obstacleGrid[i][0] != 1:
dp[i][0] = 1
else:
break
print(dp)
## 按照转移方程更新dp数组
for i in range(1, m):
for j in range(1, n):
# print(i, j)
if obstacleGrid[i][j] == 1:
dp[i][j] = 0
else:
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
print(dp)
return dp[-1][-1]
343. 整数拆分
1. dp数组以及下标的含义:
一维数组,每个n对应的最大的乘积值
2. 状态转移方程
对于一个数n,有2种拆分方式,1. 直接拆分成 n - j 和 j;2. 拆分出j之后,对j继续拆分(取j能够被拆分获得的最大乘积)。
由于在每个i的位置我们会将j从1遍历致i,过程反复更新i,因此dp[i]本身的过程极大值也是需要被记录的。
综上,状态转移方程由三个元素构成:
python
dp[i] = max(dp[i], j * dp[i - j], j * (i - j))
3. dp数组初始化
全0初始化,只对i = 0; i = 1做起始赋值即可
4. 确定遍历顺序
顺序遍历
5. 校验dp变化过程
[0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 1, 1, 2, 0, 0, 0, 0, 0, 0, 0]
[0, 1, 1, 2, 4, 0, 0, 0, 0, 0, 0]
[0, 1, 1, 2, 4, 6, 0, 0, 0, 0, 0]
[0, 1, 1, 2, 4, 6, 9, 0, 0, 0, 0]
[0, 1, 1, 2, 4, 6, 9, 12, 0, 0, 0]
[0, 1, 1, 2, 4, 6, 9, 12, 18, 0, 0]
[0, 1, 1, 2, 4, 6, 9, 12, 18, 27, 0]
[0, 1, 1, 2, 4, 6, 9, 12, 18, 27, 36]
实现:
python
class Solution:
def integerBreak(self, n: int) -> int:
dp = [0] * (n + 1)
dp[0] = 0
dp[1] = 1
for i in range(2, n + 1):
for j in range(1, i):
dp[i] = max(dp[i], j * dp[i - j], j * (i - j))
print(dp)
return dp[-1]
96. 不同的二叉搜索树
1. dp数组以及下标的含义
看似复杂实际上主要考验思维,观察发现,实际上n = 1包含在n = 2中,而n = 2 又包含在n = 3中。具体来说,设置一个一维dp数组,dp[n] 表示当前n中存在的二叉搜索树种类数。
dp[3]中,节点1,2,3分别做了一次根节点,1和3做根节点时,下方子数的排布(非数值)包含了dp[2]的两个状态。难点是 2做根节点,下方的子树排列其实是dp[1]的形式。
dp[3],就是 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量
元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 * 左子树有0个元素的搜索树数量
元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 * 左子树有1个元素的搜索树数量
元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量
dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]
2. 状态转移方程
dp[n] 表示当前的数值,而dp[n]的值是由不同的 根节点值及其左右子树组合而来的。dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量。也就是说,其实不考虑具体的值,而是只考虑给定节点数的子结构数量。
3. dp数组初始化
全0初始化后对 n=0,1,2 情况赋值
4. 确定遍历顺序
顺序遍历,从3开始遍历,每个i还需要从1开始搜索不同值作为根节点的情况
5. 校验dp变化过程
n值 头结点 dp数组
3 1 [1, 1, 2, 2]
3 2 [1, 1, 2, 3]
3 3 [1, 1, 2, 5]
实现:
python
class Solution:
def numTrees(self, n: int) -> int:
if n < 2:
return 1
dp = [0] * (n + 1)
dp[0] = dp[1] = 1
dp[2] = 2
for i in range(3, n + 1):
for j in range(1, i + 1):
print(i, j)
dp[i] += dp[i - j] * dp[j - 1]
print(dp)
return dp[-1]
46. 携带研究材料(0-1背包问题)
1. dp数组以及下标的含义
二维dp数组。列维度代指物品,行维度代指背包容量,dp[i][j] 代表任取物品 0 ~ i,在背包容量为j时所能取得的最大价值。如:任取 物品0,物品1 放进容量为4的背包里,最大价值是 dp[1][4]。由于列维度表示物品,因此每一行中能获取的物品取决于当前的i值。
2. 状态转移方程
每个格点dp[i][j]有两个状态:1. 不放物品,背包内的总值是[i - 1][j];2. 放物品,状态转移从[i - 1][j - weight[i]]得到,使用该格点的值加上物品i的价值得到。[j - weight[i]] 可以直接避免当前物品装不下的情况。使用max函数确保每次更新都可以获得最大收益。
3. dp数组初始化
初始化需要满足0-1背包的基本条件,我们可以引入背包重量0来避开一些边界条件,但是物品重量是需要被包括在初始数组里的(多第0个无容量列,但物品数量不变)。
4. 确定遍历顺序
使用二维数组可以进行顺序遍历,且先遍历物品或者背包容量都可以。我们选择先遍历物品(每一列)
5. 校验dp变化过程
背包内容无需排序,在二维状态转移中我们能够在任意位置记录价值最大结果
实现:
python
while True:
try:
n, bagsize = list(map(int, input().split()))
weight = list(map(int, input().split()))
value = list(map(int, input().split()))
## 初始化dp数组
dp = [[0] * (bagsize + 1) for _ in range(n)]
for j in range(weight[0], bagsize + 1):
dp[0][j] = value[0]
# print(dp)
## 物品id
for i in range(1, n):
## 背包容量
for j in range(1, bagsize + 1):
if j < weight[i]:
dp[i][j] = dp[i - 1][j]
else:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
print(dp[n - 1][bagsize])
except EOFError:
break
使用一维dp数组做状态压缩 :
使用一维dp时,可以理解为不断将上一个物品的最大值反转到下一个物品的dp数组,从而实现状态压缩。
在具体实现上,先顺序遍历物品,嵌套倒序遍历背包。之所以要倒序,是因为倒序遍历,就可以保证物品只放入一次。
例如:倒序就是先算dp[2]
dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
dp[1] = dp[1 - weight[0]] + value[0] = 15
这个规律是根据dp的状态转移方程决定的,利用倒序遍历巧妙避开了从左向右叠加的问题。
python
while True:
try:
n, bagsize = list(map(int, input().split()))
weight = list(map(int, input().split()))
value = list(map(int, input().split()))
## 初始化dp数组
dp = [0] * (bagsize + 1)
dp[0] = 0
## 物品id从0开始遍历 背包容量倒序遍历
for i in range(n):
for j in range(bagsize, weight[i] - 1, -1):
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
print(dp[-1])
except EOFError:
break
416. 分割等和子集
1. dp数组以及下标的含义
一维数组,容量为sum(nums) / 2时,当容量为j时能装入的最大数值
2. 状态转移方程
0-1背包一维数组转移方程:
python
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
3. dp数组初始化
dp[0] = 0 初始化即可
4. 确定遍历顺序
顺序遍历元素,倒序遍历背包
5. 校验dp变化过程
只要dp[j] = 目标值就返回true,否则最终返回false
实现:
python
class Solution:
def canPartition(self, nums: List[int]) -> bool:
if sum(nums) % 2 == 1:
return False
else:
bag_size = sum(nums) // 2
## 数字的数值同时等于物品价值和体积
dp = [0] * (bag_size + 1)
dp[0] = 0
## 遍历物品
for i in range(len(nums)):
## 遍历背包容积
for j in range(bag_size, nums[i] - 1, -1):
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
if dp[j] == bag_size:
return True
return False