初见DP

线性DP

例题1

1143. 最长公共子序列 (LCS)

子序列不连续

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例 1:

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。

本质是选或不选

dfs(i,j)表示s的前i个字母和t的前j个字母的LCS长度

当s[i]=t[j]的时候,dfs(i-1,j-1)+1是要大于前面两个的,舍弃前两个

s[i]!=t[j]的时候,前面两个会自动递归到 dfs(i-1,j-1)+1,因此舍弃

代码:

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        x=len(text1)
        y=len(text2)
        @cache
        def dfs(n,m):
            if n<0 or m<0:
                return 0
            if text1[n]==text2[m]:
                return (dfs(n-1,m-1)+1)
            if text1[n]!=text2[m]:
                return max(dfs(n-1,m),dfs(n,m-1))

        return dfs(x-1,y-1)
        

改成数组:注意避免负数下标,这里都往后移一位

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        x1=len(text1)
        y1=len(text2)
        f=[[0]*(y1+1)  for _ in range(x1+1)]
        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[x1][y1]
        
      

72. 编辑距离

给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数  。

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

示例 1:

输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

思路:从末尾值开始比较,如果s[i]=t[j]  直接dfs(i-1.j-1),因为不能改变这个值已经想同了

删除一个字母:相当于去掉s[i],也就是dfs(i,j-1)

插入一个字母,插入的肯定是和t[j]是一样的,所以是dfs[i+1,j],因为此时s[i]=t[j] ,所以同时减去1,得到最终答案dfs(i.j-1)

替换:把s【i】替换成t【j】,在同时减去1,也就是dfs(i-1,j-1)+1

代码:

class Solution:
    def minDistance(self, word1: str, word2: str) -> int:
        n=len(word1)
        m=len(word2)
        @cache
        def dfs(i,j):
            if i<0:
                return j+1
            if j<0:
                return i+1
            if word1[i]==word2[j]:
                return dfs(i-1,j-1)
           
            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, s: str, t: str) -> int:
        n, m = len(s), len(t)
        f = [[0] * (m + 1) for _ in range(n + 1)]
        f[0] = list(range(m + 1))
        for i, x in enumerate(s):
            f[i + 1][0] = i + 1
            for j, y in enumerate(t):
                f[i + 1][j + 1] = f[i][j] if x == y else \
                                  min(f[i][j + 1], f[i + 1][j], f[i][j]) + 1
        return f[n][m]

#f[0] = list(range(m + 1))表示初始化第一行,即s为空字符串时与t的编辑距离。因为此时s为空,所以只需要插入操作即可将t转换为s,所以第一行从0开始递增。


f[i + 1][0] = i + 1表示初始化第一列,即t为空字符串时与s的编辑距离。因为此时t为空,所以只需要删除操作即可将s转换为t,所以第一列从0开始递增。

300. 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的

子序列

。 

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

递归代码:

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        n=len(nums)
        @cache
        def dfs(i):
            res=0
            for x in range(i):
                if nums[x]<nums[i]:
                    res= max(res,dfs(x))
            return res+1
        ans=0
        
        return max(dfs(i) for i in range(n))

转化成数组
 

class Solution:
    def lengthOfLIS(self, nums: List[int]) -> int:
        n=len(nums)
        f=[0]*n
        for i in range(n):
            for j in range(i):
                if nums[j]<nums[i]:
                    f[i]=max(f[j],f[i])
            f[i]+=1
        return max(f)

        

状态机DPww

122. 买卖股票的最佳时机 II

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润 。

示例 1:

输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。
最大总利润为 4 + 3 = 7

从表示状态之间转换关系的图叫状态机

因为第i-1天结束也就是i天开始,所以dfs(i-1,-)也表示第i天开始时的最大利润

递归边界: dfs(-1,0)=0 第0天开始未持有股票,利润为0,dfs(-1,1)=-inf,第0天开始不可能有股票

递归入口:dfs(n-1,0)

递归+记忆化搜索

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n=len(prices)
        @cache
        def dfs(n,hold):
            if n<0:
                return -inf if hold else 0
            if hold:
                return max(dfs(n-1,True),dfs(n-1,False)-prices[n])
            return max(dfs(n-1,False),dfs(n-1,True)+prices[n])
        return dfs(n-1,False)

数组+递推

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


           

变型:

309. 买卖股票的最佳时机含冷冻期

给定一个整数数组prices,其中第  prices[i] 表示第 i 天的股票价格 。​

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入: prices = [1,2,3,0,2]
输出: 3 
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

首先在没有冷冻期的情况下,dp【i】【1】是从dp【i-1】【0】和dp【i-1】【1】转移过来的,然后有冷冻期时,仍然可以从dp【i-1】【1】转移过来,至于dp【i-1】【0】,我们只需要考虑第i-2天也没有持有的情况,而如果连续两天没有持有,等价于这两天没有任何操作,所以dp【i-1】【0】和dp【i-2】【0】是一样的。所以dp【i-2】【0】就是这种情况,直接从dp【i-2】【0】转移过来即可,只有这一种情况可以打通dp【i-2】【0】到dp【i-1】【0】再到dp【i】【1】这条道路,所以dp【i-2】【0】和dp【i-1】【1】已经包含了所有合法的情况。

变型2

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n=len(prices)
        @cache
        def dfs(i,hold):

            if i<0:return -inf if hold else 0
            if hold :
                return max(dfs(i-1,True),dfs(i-2,False)-prices[i])
            return max(dfs(i-1,False),dfs(i-1,True)+prices[i])
        return dfs(n-1,False)

188. 买卖股票的最佳时机 IV

买+卖=一次交易

给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

j<0 返回-inf  dfs(-1,j,0)=0   dfs(-1,j,1)=-inf

递归入口: dfs(n-1,k,0)

递归

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,False)-prices[i])
            return max(dfs(i-1,j,False),dfs(i-1,j-1,True)+prices[i])
        return dfs(n-1,k,False)

递推

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

递归中 j 是从 -1 到 k,所以有 k+2 个不同的 j,翻译的时候这个维度的长度就是 k+2 了

k+1 是因为需要在前面插入一个状态表示 j=-1 的情况,所以要把原来的 i 改成 i+1,k 改成 k+1。

区间DP

从小区间转移到大区间

516. 最长回文子序列

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:

输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。

示例 2:

输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。

思路1:求s和s反转后的LCS

思路2: 从两侧向内缩小问题规模(选还是不选)

边界: dfs(i,i)=1,本身就是一个回文子序列

        dfs(i+1,i) 没有字母,返回0

入口:dfs(0,n-1)

代码:

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        n=len(s)
        @cache
        def dfs(i,j):
            if i>j:
                return 0
            if i==j:
                return 1
            if s[i]==s[j]:
                return dfs(i+1,j-1)+2
            return max(dfs(i+1,j),dfs(i,j-1))
        return dfs(0,n-1)

递推:

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):
            f[i][i] = 1
            for j in range(i + 1, n):
                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]

1039. 多边形三角剖分的最低得分

你有一个凸的 n 边形,其每个顶点都有一个整数值。给定一个整数数组 values ,其中 values[i] 是第 i 个顶点的值(即 顺时针顺序 )。

假设将多边形 剖分 为 n - 2 个三角形。对于每个三角形,该三角形的值是顶点标记的乘积,三角剖分的分数是进行三角剖分后所有 n - 2 个三角形的值之和。

返回 多边形进行三角剖分后可以得到的最低分 。
 

示例 1:

输入:values = [1,2,3]
输出:6
解释:多边形已经三角化,唯一三角形的分数为 6。

示例 2:

输入:values = [3,7,4,5]
输出:144
解释:有两种三角剖分,可能得分分别为:3*7*5 + 4*5*7 = 245,或 3*4*5 + 3*4*7 = 144。最低分数为 144。

  代码:

class Solution:
    def minScoreTriangulation(self, values: List[int]) -> int:
        n=len(values)
        @cache
        def dfs(i,j):
            if i+1==j:
                return 0
            res=inf
            for k in range(i+1,j):
                res=min(res,dfs(i,k)+dfs(k,j)+values[i]*values[k]*values[j])
            return res
        return dfs(0,n-1)

 代码:

 树形DP

543. 二叉树的直径

给你一棵二叉树的根节点,返回该树的 直径 。

二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。

两节点之间路径的 长度 由它们之间边数表示。

示例 1:

输入:root = [1,2,3,4,5]
输出:3
解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。

示例 2:

输入:root = [1,2]
输出:1

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
        ans=0
        def dfs(node):
            if node is None:
                return -1
            left=dfs(node.left)
            right=dfs(node.right)
            nonlocal ans
            ans=max(ans,left+right+2)
            return max(left,right)+1
        dfs(root)
        return ans

124. 二叉树中的最大路径和

二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和 。

示例 1:

输入:root = [1,2,3]
输出:6
解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6

示例 2:

输入:root = [-10,9,20,null,null,15,7]
输出:42
解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def maxPathSum(self, root: Optional[TreeNode]) -> int:
        ans=-inf
        def dfs(node):
            if node is None:
                return 0
            l_val=dfs(node.left)
            r_val=dfs(node.right)
            nonlocal ans
            ans=max(ans,l_val+r_val+node.val)
            return max(max(l_val,r_val)+node.val,0)
        dfs(root)
        return ans

为什么要和0比最大值,因为可能算出来的是负数,比如一个树是1,-1,2,那么答案肯定是1,2.但是这里算出来的左子树max是-1,因此要和0比较,小于0等于不选

2246. 相邻字符不同的最长路径

给你一棵 (即一个连通、无向、无环图),根节点是节点 0 ,这棵树由编号从 0 到 n - 1 的 n 个节点组成。用下标从 0 开始、长度为 n 的数组 parent 来表示这棵树,其中 parent[i] 是节点 i 的父节点,由于节点 0 是根节点,所以 parent[0] == -1 。

另给你一个字符串 s ,长度也是 n ,其中 s[i] 表示分配给节点 i 的字符。

请你找出路径上任意一对相邻节点都没有分配到相同字符的 最长路径 ,并返回该路径的长度。

示例 1:

输入:parent = [-1,0,0,1,1,2], s = "abacbe"
输出:3
解释:任意一对相邻节点字符都不同的最长路径是:0 -> 1 -> 3 。该路径的长度是 3 ,所以返回 3 。
可以证明不存在满足上述条件且比 3 更长的路径。 

class Solution:
    def longestPath(self, parent: List[int], s: str) -> int:
        n=len(parent)
        g=[[]for _ in range(n)]
        for i in range (1,n):
            g[parent[i]].append(i)
            
        ans=0

        def dfs(x):
            nonlocal ans
            x_len=0
            for y in g[x]:
                y_len=dfs(y)+1
                if s[y]!=s[x]:
                    ans=max(ans,x_len+y_len)
                    x_len=max(x_len,y_len)
            return x_len
        dfs(0)
        return ans+1
        

337. 打家劫舍 III

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

示例 1:

输入: root = [3,2,3,null,3,null,1]
输出: 7 
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7

示例 2:

输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9

最大独立集需要从图中选择尽量多的点,他们互不相邻

变型:最大化点权之和

树和子树关系类似于原问题和子问题,如何由子问题算出原问题

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def rob(self, root: Optional[TreeNode]) -> int:
        def dfs(node):
            if node is None:
                return 0,0
            l_rob,l_not_rob=dfs(node.left)
            r_rob,r_not_rob=dfs(node.right)
            rob=l_not_rob+r_not_rob+node.val
            not_rob=max(l_not_rob,l_rob)+max(r_rob,r_not_rob)
            return rob,not_rob
        return max(dfs(root))

P1352 没有上司的舞会 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

输入

7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5

输出 5

代码部分(c++)

968. 监控二叉树

给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量

示例 1

输入:[0,0,null,0,null,0,null,null,0]
输出:2
解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。

当前节点是蓝色,表示选择安装谁摄像头

当前节点是黄色,表示选择不安装,且它的父节点安装摄像头

当前节点是红色,表示选择不安装,且它的至少一个儿子安装摄像头

对于蓝色节点,他的儿子装不装摄像头都可以

对于黄色节点,他的儿子不能是黄色,因为黄色节点的父节点必须要安装摄像头

对于红色节点,他的儿子不可能是黄色,因为它本身没有安装摄像头

因此

最终代码:

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def minCameraCover(self, root: Optional[TreeNode]) -> int:
        def dfs(node):
            if node is None:
                return inf,0,0
            l_choose,l_fa,l_child=dfs(node.left)
            r_choose,r_fa,r_child=dfs(node.right)
            choose=min(l_choose,l_fa,l_child)+min(r_choose,r_fa,r_child)+1
            fa=min(l_choose,l_child)+min(r_choose,r_child)
            child=min(l_choose+r_choose,l_choose+r_child,l_child+r_choose)
            return choose,fa,child
        choose,_,child=dfs(root)
        return min(choose,child)


 变型1:在节点x安装摄像头需要花费cost【x】,那么只需要把+1改成加cost【node】

变型2 优化红色:如果子节点很多没那么红色要罗列的情况就很多,如果3个子节点,那就要考虑2的3次方-1次情况

因为我们优化成

当红色都比蓝色小的时候,都选择红色,所以要添加一个max(0,min(蓝1-红1,蓝2-红2,蓝3-红3),也就是补上差值,让其中一个红色变成蓝色

课后作业:洛谷保安站岗

  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值