Day12,Hot100(动态规划)

将递归转为递推时,我们通常会对 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] 物品中选择出的最大值

    • 不放物品 if[i][j] = f[i-1][j]
    • 放物品 if[i][j] = f[i-1][j-v[i]] + nums[i]
  • 数组初始化(考虑 0 下标,即 i==0j==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
  • 遍历顺序:for 物品 --> for 容量(倒序遍历)
    • 如果是正序遍历,物品会被重复计算

(1)416. 分割等和子集

在这里插入图片描述

因为要将 nums 划分为为两段,每段的和相等

  1. 所以先判断,数组的总和 s 是否是偶数
  2. 如果是偶数,则问题变为:是否能找到一段前缀的和为 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
    在这里插入图片描述
    在这里插入图片描述
  1. s[i] = t[j]时,需要考虑只选 i 或者只选 j 吗?dfs(i-1,j) dfs(i,j-1)
    • 只需要考虑都选 ,因为只选其中一个的值一定是 <= 都选的情况的
  2. 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未持有, 未操作>
      在这里插入图片描述

边界

  • 第 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+1j-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.leftnode.right 不能选
    • 如果不选 node,则 node.leftnode.right 能选,
      • 于是最大值可以为 max(l_rob, l_no) + max(r_rob, r_no)
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 中

递归

  1. 主问题:求 s 能否拆分为多段,且每段都在 word 中
  2. 子问题: 子问题 dfs(i) 表示能否将前缀 s[0:i] 这一段划分为若干段,且每段都在 wordDict 中
  3. 边界:当前缀为空时,说明之前的都符合
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]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值