将递归转为递推时,我们通常会对 f 增加一个状态,
导致下标由 0~n-1,平移到 1~n,
此时我们只需修改与f相关的下标即可
因为我们是对 f 增加一个状态,不影响其他的
动态规划
(1)70. 爬楼梯

class Solution:
def climbStairs(self, n: int) -> int:
f = [0] * (n+1) # 爬到第 n 阶的方案数
f[0] = f[1] = 1
for i in range(2, n+1):
f[i] = f[i-1] + f[i-2]
return f[n]
(2)118. 杨辉三角

[1]
[1,1]
[1,2,1]
[1,3,3,1]
[1,4,6,4,1]
f[i][j] = f[i-1][j-1] + f[i-1][j]
第1列和i==j时,对应元素都为1
class Solution:
def generate(self, numRows: int) -> List[List[int]]:
f = [[1] * (i+1) for i in range(numRows)]
for i in range(2, numRows):
for j in range(1, i):
f[i][j] = f[i-1][j-1] + f[i-1][j]
return f
(3)198. 打家劫舍
递归中dfs[i]和动态规划中f[i],都表示一些元素的性质(比如前i个元素的xx)
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
f0 = 0 # 从前(i-2)个元素中求得的最大和
f1 = 0 # 从前(i-1)个元素中求得的最大和
ans = 0
for i in range(n):
ans = max(f0 + nums[i], f1)
f0 = f1
f1 = ans
return ans
(4)213. 打家劫舍 II

- 判断边界
nums[0]是否要选 - 如果选
nums[0], 则 1 和 -1 不能选 - 如果不选, 则相当于是在剩余的
nums[1:]中选
class Solution:
def f(self, nums):
n = len(nums)
f0 = f1 = 0 # f0前两个位置的最大值, f1前一个位置的最大值
f = 0 # 当前位置的最大值
for i in range(n):
f = max(f0 + nums[i], f1)
f0, f1 = f1, f
return f
def rob(self, nums: List[int]) -> int:
# 判断边界nums[0]是否要选
# 如果选nums[0], 则 1 和 -1 不能选
choose = nums[0] + self.f(nums[2:-1])
# 如果不选, 则相当于是在剩余的[1:]中选
no_choose = self.f(nums[1:])
return max(choose, no_choose)
(5)64. 最小路径和

- 初始化
f[i][j] = inf,保证取 min 的时候可以正常取 - 左上角起点值
if i == 0 and j == 0: f[i+1][j+1] = grid[i][j]
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
# 左上角 --> 右下角, 路径和最小
# f[i][j] = min(f[i-1][j], f[i][j-1])
m, n = len(grid), len(grid[0])
f = [ [inf]*(n+1) for _ in range(m+1) ]
for i in range(m):
for j in range(n):
# 左上角开始
if i == 0 and j == 0:
f[i+1][j+1] = grid[i][j]
else:
f[i+1][j+1] = min(f[i][j+1], f[i+1][j]) + grid[i][j]
return f[m][n]
(6)62. 不同路径

class Solution:
def uniquePaths(self, m: int, n: int) -> int:
# f[i][j] 表示走到 [i,j] 一共有的路径数
# f[i][j] = f[i-1][j] + f[i][j-1]
f = [ [0]*(n+1) for _ in range(m+1) ]
for i in range(m):
for j in range(n):
if i == 0 and j == 0:
f[i+1][j+1] = 1
else:
f[i+1][j+1] = f[i][j+1] + f[i+1][j]
return f[m][n]
(7)32. 最长有效括号
class Solution:
def longestValidParentheses(self, s: str) -> int:
# 1.匹配成功, 先找到所有可以匹配成功的括号下标
# 2.最长连续, 然后计算所有可以匹配中的最长连续值
stack=[]
ans=0
n=len(s)
tmp=[0]*n #标记数组
# 把左括号入栈, 逐个匹配相印的右括号
for i in range(n):
if s[i]=='(':
stack.append(i)
else:
# 此时右括号和栈顶的左括号匹配成功
if stack:
j = stack.pop()
tmp[i],tmp[j]=1,1 #匹配成功时标记
cur=0 # 当前最长有效括号
for num in tmp:
if num == 1:
cur+=1
else: # 遇到0时中断, 记录最大值
ans=max(cur,ans)
cur=0
ans=max(cur, ans) #最后一次统计可能未终断,多做一次对比
return ans
0-1背包


-
0,1背包暴力做法:回溯,每个物品判断取和不取两种状态
-
时间复杂度为 O(2^n)【每个物品两种状态】
-
f[i][j]:背包容量为j时,从[0, i]物品中选择出的最大值- 不放物品
i:f[i][j] = f[i-1][j] - 放物品
i:f[i][j] = f[i-1][j-v[i]] + nums[i]
- 不放物品
-
数组初始化(考虑 0 下标,即
i==0或j==0的情况)f[i][j] = max(f[i][j] = f[i-1][j], f[i-1][j-v[i]] + nums[i])- 计算
i,j时,需要正上方和左上方的值 - 所以只要正上方和左上方的值被计算出来了,就可以得到
f[i][j], - 所以先遍历物品再遍历背包或者先遍历背包再遍历物品都可以

- 一维数组:计算
f[i][j]的时候,其实只用到了上一行,即[i-1] - 所以相当于每次把上一行的结果拷贝到当前行,然后利用当前行再计算就可以实现状态压缩
f[j]:容量为j的数组的最大值- 不放物品
i=f[j] - 放物品
i=f[j-v[i]] + nums[i] - 初始化,
f 全部 = 0,因为f[j] = max(f[j], f[j-v[i]] + nums[i])- 要对自己取 max,所以初始值要是
0
- 要对自己取 max,所以初始值要是
- 不放物品
- 遍历顺序:for 物品 --> for 容量(倒序遍历)
- 如果是正序遍历,物品会被重复计算
(1)416. 分割等和子集

因为要将 nums 划分为为两段,每段的和相等
- 所以先判断,数组的总和 s 是否是偶数
- 如果是偶数,则问题变为:
是否能找到一段前缀的和为 s//2(剩下的和自然也为 s // 2)
- 递归定义:
dfs(i, j),能否从[0, i]中选出一个恰好等于为j的子序列- 于是对于
num[i]可以有选 / 不选- 选,下一个子问题为:从
[0, i-1]中选出一个恰好等于为j-num[i]的子序列,即dfs(i-1, j-num[i]) - 不选,
dfs(i-1, j)
- 选,下一个子问题为:从
递归
from functools import cache
class Solution:
def canPartition(self, nums: List[int]) -> bool:
s = sum(nums)
if s % 2 != 0:
return False
s //= 2
n = len(nums)
@cache
def dfs(i, j): # 从[0,i]中能否找到子序列的和为 j
if j == 0:
return True
if i < 0:
return False
if j >= nums[i]:
# 剩余和够的时候, 才有选和不选
return dfs(i-1, j-nums[i]) or dfs(i-1, j)
else:
# 否则只能不选
return dfs(i-1, j)
递推

from functools import cache
class Solution:
def canPartition(self, nums: List[int]) -> bool:
s = sum(nums)
if s % 2 != 0:
return False
s //= 2
n = len(nums)
f = [ [False]*(s+1) for _ in range(n+1) ] # f[i][j], 从[0,i]中能否找到子序列的和为 j
for i in range(n+1):
f[i][0] = True # 对于和为 0 的情况, 所有序列都满足
for i, x in enumerate(nums):
for j in range(1, s+1):
if j >= x: # 当前容量可以装下
f[i+1][j] = f[i][j-x] or f[i][j]
else:
f[i+1][j] = f[i][j]
return f[n][s]
(2)494. 目标和

- nums 总和 s
- 所有添加 + 号的数之和
p - 所有添加 - 号的数之和
s-p - 于是,
target = + p - (s-p) 2p-s = target-->p = (s+target) / 2【s和target都是已知值】- 于是问题转换为了, 求 “选任意个数和为p的方案数”
- 方案数不可能为小数或负数, 所以当
(s+t)<0或(s+t)为奇数时, 方案数为0
递归
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
s = sum(nums)
p = target + s
if p < 0 or p % 2 != 0:
return 0
target = p // 2 # 目标和
@functools.cache
def dfs(i, j): # 从 [0,i] 中选择任意的数, 和是否为 j
if i == -1:
if j == 0: # 方案数 +1
return 1
return 0
if nums[i] <= j:
# 当前体积 j 能够装下物品 nums[i]
# 则有 不装 + 装 两种
return dfs(i-1, j) + dfs(i-1, j-nums[i])
else:
return dfs(i-1, j)
return dfs(len(nums)-1, target)
递推
剩余容量不够时, f[i+1][c] = f[i][c]
剩余容量足够时,f[i+1][c] = f[i][c] + f[i][c-v]
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
s = sum(nums)
p = target + s
if p < 0 or p % 2 != 0:
return 0
target = p // 2 # 目标和
n = len(nums)
# f[i][j], 从前i个数, 选出和为 j 的方案数
f = [ [0]*(target+1) for _ in range(n+1) ]
# 初始化, 对于和为 0 的情况, 方案数都为1(一个都不选)
f[0][0] = 1
for i, x in enumerate(nums):
for j in range(target+1):
if x <= j:
# 选或不选
f[i+1][j] = f[i][j-x] + f[i][j]
else:
f[i+1][j] = f[i][j]
return f[n][target]
完全背包
物品可以重复选择


(1)279. 完全平方数

需要写在类外面,才可以通过
递归
# 写在外面,多个测试数据之间可以共享,减少计算量
@cache # 缓存装饰器,避免重复计算 dfs 的结果(记忆化)
def dfs(i,j):
if i == 0:
if j == 0:
return 0
return inf
if i * i > j:
return dfs(i-1, j) # 则当前数一定不能选
# 选i
x1 = dfs(i, j-i*i) + 1 # 可以重复选同一个数, 所以 i 不用减一
# 不选i
x2 = dfs(i-1, j)
return min(x1, x2)
class Solution:
def numSquares(self, n: int) -> int:
return dfs(isqrt(n), n)
从递归到递推 :
- 对于选的情况,记得
+1
class Solution:
def numSquares(self, n: int) -> int:
# f[i][j] 从前i个数中选择和为 j 的最少数目
num = isqrt(n)
f = [[inf for j in range(n + 1)] for i in range(num + 1)]
f[0][0] = 0
for i in range(1, num + 1):
for j in range(n+1):
v = i * i
if v > j: # 只能不选
f[i][j] = f[i-1][j]
else:
f[i][j] = min(f[i-1][j], f[i][j-v] + 1)
return f[num][n]
(2)322. 零钱兑换

与 <279. 完全平方数> 一样的思路
- 从 0~i 个数中选择最少的数,使得结果的和为 j
首先是递归
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
@cache # 缓存装饰器,避免重复计算 dfs 的结果(记忆化)
def dfs(i,j): # 从1~i面额中选择面值为j的个数
if i < 0:
if j == 0:
return 0
else:
return inf
# 只能不选
if coins[i] > j:
return dfs(i-1, j)
# 不选
x1 = dfs(i-1, j)
# 选
x2 = dfs(i, j-coins[i]) + 1
return min(x1, x2)
ans = dfs(len(coins)-1, amount)
return ans if ans < inf else -1
递归转为递推
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
n = len(coins)
f = [[inf] * (amount + 1) for _ in range(n+1)]
f[0][0] = 0
for i in range(n):
for j in range(amount + 1):
x = coins[i]
if x > j:
f[i + 1][j] = f[i][j]
else:
f[i + 1][j] = min(f[i][j], f[i + 1][j-x] + 1)
ans = f[n][amount]
return ans if ans < inf else -1
线性DP
(1)1143. 最长公共子序列

从两个字符串的末尾开始选择,一共有4种决策方式
- 都选
- 都不选
- 选x/y


s[i] = t[j]时,需要考虑只选 i 或者只选 j 吗?dfs(i-1,j) dfs(i,j-1)- 只需要考虑都选 ,因为只选其中一个的值一定是 <= 都选的情况的
s[i] ≠ t[j]时,需要考虑都不选的情况吗dfs(i-1,j-1)- 对于
dfs(i-1,j),选 j 或不选 j 的下一个状态就是dfs(i-1,j-1),同理对于dfs(i,j-1)也是 - 所以
dfs(i-1,j) dfs(i,j-1)已经包含了dfs(i-1,j-1)的结果,不需要
- 对于
递归代码
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
n, m = len(text1), len(text2)
@cache
def dfs(i, j):
if i < 0 or j < 0:
return 0
if text1[i] == text2[j]:
return dfs(i-1, j-1) + 1
else:
return max(dfs(i-1, j), dfs(i, j-1))
return dfs(n-1, m-1)
翻译为递推
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
n, m = len(text1), len(text2)
f = [ [0] * (m+1) for _ in range(n+1) ] # f[i][j] 表示0~i和0~j的最长公共子序列
for i, x in enumerate(text1):
for j, y in enumerate(text2):
if x == y:
f[i+1][j+1] = f[i][j] + 1
else:
f[i+1][j+1] = max(f[i][j+1], f[i+1][j])
return f[n][m]
(2)72. 编辑距离

从右端开始判断
1.将word1中的词替换为word2中的词 --> dfs(i-1, j-1)
2. 将word1中的词删除 --> dfs(i-1, j)
3. 将word2中的词插入word1 --> dfs(i, j-1)

x == y, 无需操作,dfs(i-1, j-1)x != y, 从插入,删除,替换中选一个操作最少的
递归
if i < 0: return j + 1
因为,当 word1 为空时, 只有把 word2 的词都去掉, 也变为空,两者才会相等
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
n, m = len(word1), len(word2)
@cache
def dfs(i, j): #将 word1[:i] 转换为 word2[:j] 需要的最少步数
if i < 0: # word1 为空时, 只有把word2的词都去掉, 也变为空
return j + 1
if j < 0:
return i + 1
if word1[i] == word2[j]:
return dfs(i-1, j-1)
else:
return min(dfs(i-1,j), dfs(i,j-1), dfs(i-1,j-1)) + 1
return dfs(n-1, m-1)
递推
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
n, m = len(word1), len(word2)
f = [ [inf]*(m+1) for _ in range(n+1) ]
for i in range(n):
f[i+1][0] = i+1
for j in range(m):
f[0][j+1] = j+1
for i, x in enumerate(word1):
for j, y in enumerate(word2):
if x == y:
f[i+1][j+1] = f[i][j]
else:
f[i+1][j+1] = min(f[i][j+1], f[i+1][j], f[i][j]) + 1
return f[n][m]
(3)300. 最长递增子序列

从尾部开始遍历,dfs(i)表示以i结尾的最长递增子序列
- 子问题为:下一个位置 j 为结尾的最长递增子序列,
dfs(j)


递归
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
n = len(nums)
@cache
def dfs(i): # 以i结尾的 最长递增子序列
if i < 0:
return 0
ans = 1 # 当前 i 的长度
for j in range(i):
if nums[j] < nums[i]:
ans = max(ans, dfs(j) + 1)
return ans
ans = 0
for i in range(n):
ans = max(ans, dfs(i))
return ans
递推
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
n = len(nums)
f = [0] * n # 以 i 为结尾的最长递增子序列
for i in range(n):
f[i] = 1 # i 元素本身的长度
for j in range(i):
if nums[j] < nums[i]:
f[i] = max(f[i], f[j] + 1)
return max(f)
状态机DP
(1)122. 买卖股票的最佳时机 II——不限制交易次数

从最后一天往前思考,方便从递归修改为递推


dfs(i, 0): 表示到第 i 天结束的时候,未持有股票 (0) 的最大利润
( 0 / 1,分别表示未持有和持有股票)
-
如果持有股票
- 上一个状态可能是 <i-1持有,未操作> 或 <i-1未持有,买入>
-
如果未持有股票
- 上一个状态可能是 <i-1持有, 卖出> 或 <i-1未持有, 未操作>

- 上一个状态可能是 <i-1持有, 卖出> 或 <i-1未持有, 未操作>
边界:
- 第 0 天
开始未持有股票的状态,理论为0 - 第 0 天
开始持有股票的状态是不合法的,设为 -inf
入口
- 求最后一天持有/不持有股票的最大利润
- 然而,最后一天还持有股票,是不可能比最后一天未持有的利润高的
- 所以,
dfs(n-1, 0)即可

递归
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
@cache
def dfs(i, hold): # 到第i天<结束>时, <持有>或<未持有>股票的最大利润
if i < 0:
# 第0天开始时不可能持有股票
return -inf if hold else 0
if hold: # 如果持有股票
# 上一个状态可能是 <i-1持有,未操作> 或 <i-1未持有,买入>
return max(dfs(i-1, True), dfs(i-1,False) - prices[i])
else: # 如果未持有股票
# 上一个状态可能是 <i-1持有, 卖出> 或 <i-1未持有, 未操作>
return max(dfs(i-1, True) + prices[i], dfs(i-1, False))
return dfs(n-1, False)
递归–>递推

f[i][0]:第i天结束,不持有股票的最大利润- 前一天本来就没持有股票,
f[i-1][0] - 前一天本来持有股票,然后当天卖出了,
f[i-1][1] + prices[i]
- 前一天本来就没持有股票,
f[i][1]:第i天结束,持有股票的最大利润- 前一天本来就持有股票,
f[i-1][1] - 前一天本来就没持有股票,然后当天买入了,
f[i-1][0] - prices[i]
- 前一天本来就持有股票,
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
f = [ [0,0] for _ in range(n+1) ]
f[0][1] = -inf
for i, v in enumerate(prices):
f[i+1][1] = max(f[i][1], f[i][0] - v)
f[i+1][0] = max(f[i][1] + v, f[i][0])
return f[n][0] # 因为最后一天肯定是不持有的利润最大
(2)309. 买卖股票的最佳时机含冷冻期——卖出后第二天无法操作

参考<122. 买卖股票的最佳时机 II>,只需要在买入的状态中添加判断
即买入后要间隔一天
dfs(i, True) = max(dfs(i-1, True), dfs(i-2,False) - prices[i])
递归
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
@cache
def dfs(i, hold): # 到第i天<结束>时, <持有>或<未持有>股票的最大利润
if i < 0:
# 第0天开始时不可能持有股票
return -inf if hold else 0
if hold: # 如果持有股票
# 上一个状态可能是 <i-1持有,未操作> 或 <i-2未持有,买入>
return max(dfs(i-1, True), dfs(i-2,False) - prices[i])
else: # 如果未持有股票
# 上一个状态可能是 <i-1持有, 卖出> 或 <i-1未持有, 未操作>
return max(dfs(i-1, True) + prices[i], dfs(i-1, False))
return dfs(n-1, False)
递推
- 隔一天操作即可
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n = len(prices)
f = [ [0,0] for _ in range(n+2) ]
f[1][1] = -inf
for i, v in enumerate(prices):
f[i+2][1] = max(f[i+1][1], f[i][0] - v)
f[i+2][0] = max(f[i+1][1] + v, f[i+1][0])
return f[n+1][0]
(3)188. 买卖股票的最佳时机 IV —— 至多交易K次

至多可以买 k 次,卖 k 次

递归
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
n = len(prices)
@cache
def dfs(i, j, hold):
if j < 0:
return -inf
if i < 0:
return -inf if hold else 0
if hold:
return max(dfs(i-1, j, True), dfs(i-1, j-1, False) - prices[i])
else:
return max(dfs(i-1, j, False), dfs(i-1, j, True) + prices[i])
return dfs(n-1, k, False)
递推
- 其实就是在 <买卖股票2> 的基础上加上交易次数约束
f[i, j, 0]:第 i 天结束时, 最多完成 j 次交易,未持有股票的最大利润f[i, j, 1]:第 i 天结束时, 最多完成 j 次交易,持有股票的最大利润- 初始化
- 所有都为
-inf - 然后
f[0][j][0] = 0, 表示第 0 天的时候所有未持有状态的最大利润都是 0
- 所有都为
- 答案:肯定是未持有的情况下会利润更高
f[n][k+1][0]

class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
n = len(prices)
f = [ [[-inf,-inf] for _ in range(k+2)] for _ in range(n+1)]
for j in range(1, k+2):
f[0][j][0] = 0
for i,v in enumerate(prices):
for j in range(1, k+2):
f[i+1][j][0] = max(f[i][j][0], f[i][j][1] + v)
f[i+1][j][1] = max(f[i][j][1], f[i][j-1][0] - v)
return f[n][k+1][0]

区间DP

(1)516. 最长回文子序列

子序列 —— 非连续序列
思路一:

思路二
类似最长公共子序列
从两端开始判断,
- 左右相同时,可以都选
- 左右不同时,只能选一边


只有一个元素时(i=j),长度为 1
没有元素时(i>j),长度为 0

递归
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
n = len(s)
@cache
def dfs(i, j):
if i == j:
return 1
if i > j:
return 0
if s[i] == s[j]:
return dfs(i+1, j-1) + 2
else:
return max(dfs(i+1, j), dfs(i, j-1))
return dfs(0, n-1)
递推
f[i][j]:表示 s[i] ~ s[j] 之间的最长回文子序列- if
s[i] == s[j],f[i][j] = f[i+1][j-1] + 2 - else:
f[i][j] = max(f[i+1][j], f[i][j-1]) - 注意遍历顺序
- 计算
f[i][j]时,需要i+1和j-1 i逆序,j正序

- 计算
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
n = len(s)
f = [ [0] * n for _ in range(n)]
for i in range(n-1, -1, -1):
for j in range(n):
if i == j:
f[i][j] = 1
elif i > j:
continue
else:
if s[i] == s[j]:
f[i][j] = f[i+1][j-1] + 2
else:
f[i][j] = max(f[i+1][j], f[i][j-1])
return f[0][n-1]
(2)152. 乘积最大子数组 —— 美团一面

子数组为连续非空序列
class Solution:
def maxProduct(self, nums: List[int]) -> int:
n = len(nums)
f_max = [0] * n
f_min = [0] * n
f_max[0] = f_min[0] = nums[0]
for i in range(1, n):
x = nums[i]
f_max[i] = max(f_max[i-1] * x, f_min[i-1] * x, x)
f_min[i] = min(f_max[i-1] * x, f_min[i-1] * x, x)
return max(f_max)
(3)5. 最长回文子串——阿里一面

遍历
class Solution:
def longestPalindrome(self, s: str) -> str:
ans_left, ans_right = 0, 0
n = len(s)
# 奇回文串
for i in range(n):
l, r = i, i
while l >= 0 and r < n and s[l] == s[r]:
l -= 1
r += 1
if l + 1 <= r - 1:
if r - l - 2 > ans_right - ans_left:
ans_left = l + 1
ans_right = r - 1
# 偶回文串
for i in range(n-1):
l, r = i, i+1
while l >= 0 and r < n and s[l] == s[r]:
l -= 1
r += 1
if l + 1 <= r - 1:
if r - l - 2 > ans_right - ans_left:
ans_left = l + 1
ans_right = r - 1
return s[ans_left:ans_right+1]
(X)1039. 多边形三角剖分的最低得分


………………
树形DP
(1)543. 二叉树的直径

最长直径的起点和终点一定都是叶子结点
假设直径会经过某个点(例如上图中的 node 1)
- 那么对于当前结点,对应的最大直径为:
左子树的最大深度+右子树最大深度+2,(2表示左右子树到 node 1 的路径长度) - 而
深度 == max(左子树, 右子树) + 1, - 其实相当于是在求最大深度的过程中,更新最大直径
class Solution:
def diameterOfBinaryTree(self, root: TreeNode) -> int:
ans = 0
def dfs(node): # 以node结点的深度
if node is None:
return -1 # 叶子结点时, left + right + 2 = 0
left = dfs(node.left)
right = dfs(node.right)
nonlocal ans
ans = max(ans, left + right + 2) # 2为左右子树到根节点的长度
return max(left, right) + 1 # 左右子树的最大长度
dfs(root)
return ans
(2)124. 二叉树中的最大路径和


同样对于每个结点,计算它左右子树的最大链和
- 当前拐点的最大链和 = 左 + 右 + 当前拐点值
- 注意:当左或右为负数时,可以返回0,表示不经过该链

所以其实是求最大带权值的过程中,更新答案
class Solution:
def maxPathSum(self, root: Optional[TreeNode]) -> int:
ans = -inf
def dfs(node):
if node is None:
return 0
left = max(0, dfs(node.left))
right = max(0, dfs(node.right))
nonlocal ans
ans = max(ans, left + right + node.val)
return max(left, right) + node.val
dfs(root)
return ans
(3)2246. 相邻字符不同的最长路径
……
(4)337. 打家劫舍 III
不能偷取两个直接相连的结点

自底向上,考虑每个结点选或不选时,该结点的值


- dfs(node),返回选和不选node的值(两个值)
- 如果选 node,则
node.left和node.right不能选 - 如果不选 node,则
node.left和node.right能选,- 于是最大值可以为
max(l_rob, l_no) + max(r_rob, r_no)
- 于是最大值可以为
- 如果选 node,则
class Solution:
def rob(self, root: Optional[TreeNode]) -> int:
def dfs(node):
if node is None:
return 0,0
l_rob, l_no = dfs(node.left)
r_rob, r_no = dfs(node.right)
node_rob = l_no + r_no + node.val
node_no = max(l_rob, l_no) + max(r_rob, r_no)
return node_rob, node_no
return max(dfs(root))
(5)P1352 没有上司的舞会 ——字节一面

import sys
from collections import defaultdict
sys.setrecursionlimit(10**6) # 修改默认递归深度
n = int(sys.stdin.readline())
happiness = [0] * (n + 1)
for i in range(1, n + 1):
happiness[i] = int(sys.stdin.readline())
tree = defaultdict(list)
is_root = [True] * (n + 1)
for _ in range(n - 1):
l, k = map(int, sys.stdin.readline().split())
tree[k].append(l) # k 是 l 的上司
is_root[l] = False
root = is_root[1:].index(True) + 1 # 找到根节点
def dfs(u):
include_u = happiness[u] # 选 u
exclude_u = 0 # 不选 u
for v in tree[u]:
inc, exc = dfs(v)
include_u += exc # 选 u,则子节点 v 不能选
exclude_u += max(inc, exc) # 不选 u,则子节点 v 可选可不选
return include_u, exclude_u
print(max(dfs(root)))
划分型 DP
(1)139. 单词拆分

等价于:将 s 划分为多段,使得每段都在 wordDict 中
递归
- 主问题:求
s能否拆分为多段,且每段都在 word 中 - 子问题: 子问题
dfs(i)表示能否将前缀s[0:i]这一段划分为若干段,且每段都在 wordDict 中 - 边界:当前缀为空时,说明之前的都符合
from functools import cache
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
words = set(wordDict)
@cache
def dfs(end): # 表示前缀 s[0:end] 能否都在 wordDict 中
if end == 0: # s[0:0] 前缀为空, 说明都符合
return True
for start in range(end-1, -1, -1):
if s[start:end] in words and dfs(start):
return True
return False
return dfs(len(s)) # s[0:n] 能否都在 wordDict 中
递推
f[i]表示s[0:i]是否可拆分, 不包括 s[i]if f[j] and s[j:i] in words:说明可拆分
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
words = set(wordDict)
n = len(s)
f = [False] * (n + 1) # f[i] 表示 s[0:i]是否可拆分
f[0] = True # s[0:0] 为空串,可拆分
for i in range(1, n+1):
for j in range(i-1, -1, -1):
# 如果 s[0:j] 可以拆分(f[j] == True)
# 且 s[j:i] 是字典中的单词
if f[j] and s[j:i] in words:
f[i] = True
break
return f[n]
&spm=1001.2101.3001.5002&articleId=145536388&d=1&t=3&u=7f1f294baec341a7b8f4a785bd47b659)
1631

被折叠的 条评论
为什么被折叠?



