- 典型递归式
- 单序列或匹配型DP模板
- 数值到个数DP模板(最常考)
- 区间型DP模板
- 压缩空间
- 打印路径
典型递归式:
单序列型(与一个或多个前状态有关)
硬币个数(数值型): dp[i] = min(dp[i], dp[i - j] + 1)
偷房子: dp[i] = max(dp[i-1], dp[i-2] + nums[i - 1])
LIS: dp[i] = max(dp[i], dp[j] + 1) 1 <= j < i
Work break: dp[i] |= dp[j] and s[j:i] in word_set, 0 <= j < i
坐标型DP(与上、左等两个前状态有关)1
dp[n][m] = max{1, min(dp[n+1][m], dp[n][m+1]) - dungeon[n][m]}
匹配型(与上、左、左上等三个前状态有关)1
2正则: dp[i][j] = dp[i-1][j-1] && (p[j-1] == . || s[i-1] == p[j-1])
OR ((dp[i-1][j] && (s[i-1] == p[j-2] || p[j-2] == .)) || dp[i][j-2]) && p[j-1] == *
区间型(与该区间内以k为划分的两个子区间有关)1
dp[i][j] = max{dp[i][m] + nums[i]×nums[m]×nums[j] + dp[m][j]}, i < m < j
多状态型DP(DP有多个终止状态)1
2
3
4
5dp = dp if s[i] = '0'
= dp + 1 if s[i] = '1'
dp2 = min(dp2 + 1, dp + 1) if s[i] = '0'
= min(dp2, dp) if s[i] = '1'
解题步骤:
9zhang按照DP四部曲
- [初始化]dp数组
- [实现]
- [答案]
- [优化]是否可以用滚动内存优化空间
单序列或匹配型DP模板
实现注意事项(5点):
- [多1][初始化]初始化矩阵,dp是原长度加1。因为令边界计算方便. 遍历从多少开始,取决于前状态多少个,若递归式含dp[i-1],从1开始,如果dp[i-2]就从2开始.数值型DP,如硬币个数DP长度是数值amount,不是数组长度。Python用dp = [[0 for _ in range(M)] for _ in range(N)], M为col数,再row数
- [特殊初始值]是第一步的完善,若一维初始化dp0,若为矩阵,初始化dp[0][0]以及上和左边界。求个数dp[0] = 1, 求最值, dp[0] = 0因为递归式含+1
- [多1]跳过初始值,i从1循环到len(dp)而不是len(s), 因为用了dp[0]占位。若不用占位法,就要用if语句判断第一位的边界。大部分递归式含i-1项,所以绝大部分从1开始,从什么开始取决于递归式,这也受第二步的影响
- [少1]用到原数组s时候是s[i - 1]而不是s[i], 因为用了dp[0]占位。实现时候递归式和条件要合法,保证边界内,如dp[i - t] => i - t >=0和s[i - 1 - dp[i - 1] - 1],见最长括号数
- [答案]不一定是dp[-1],如果以某位结束的答案而不是累积结果,一般就用max_len
累积DP递归公式1
dp[n] = max(dp[n - 1], f)
Python代码:
1 | def dp(self, s): |
算法分析:
时间复杂度为O(n)
,空间复杂度O(n)
。
应用题型:
LeetCode 139 Word Break
LeetCode 044 Wildcard Matching
LeetCode 010 Regular Expression Matching
数值到个数DP模板(最常考)
求个数DP是比较简单的DP,但也是比较常考的。一般DP是前n个,这里是用数值,这点还是有点难的。
递归公式1
dp[n + num[i]] = dp[n], n = [1, tgt], i = [0, len(nums) - 1]
注意事项:
- dp[i + nums[j]] += dp[i] 而不是dp[i] + 1
- dp[0] = 1表示数值为0,可以不用任何数就能获得,所以是1种
- i从0开始,因为此题递归式用相加i + nums[j],而不是相减
- nums先排序,否则如[3, 1, 2, 4],返回dp[1] = 0, 但应该是dp[1] = 1
- 上述4点注意事项只有1和2。数
1 | def dp(self, nums, target): |
应用题型:
LeetCode 377 Combination Sum IV
一个变体是求最小个数
递归公式1
dp[n + coins[i]] = min{dp[n + coins[i]], dp[n] + 1}, n = [1, tgt], i = [0, len(nums) - 1]
不同之处:
- 递归式是dp[i] + 1
- dp初始值为最大值,而且dp[0] = 0
1 | def dp(self, coins: List[int], amount: int) -> int: |
应用题型:
LeetCode 322 Coin Change
LeetCode 871 Minimum Number of Refueling Stops
算法分析:
时间复杂度为O(n*target)
,空间复杂度O(n)
。
区间型DP模板
k循环从1还是从2开始取决于递归式
1 | def dp(self, s: str) -> int: |
应用题型:
LeetCode 516 Longest Palindromic Subsequence
LeetCode 312 Burst Balloons
压缩空间
若递归式和上一行有关,绝大部分属于此情况,就可以用两行,原数量列来处理
与标准模板不同之处:
- 行个数为2
- i仍然是遍历到所有行,所以不能用len(dp),而是len(text1) + 1.
- dp行不用i,而是i % 2
1 | def longestCommonContinuous(self, text1: str, text2: str) -> int: |
应用题型:
Karat 002 Longest Common Continuous Subarray
LeetCode 063 Unique Paths II
打印路径
三种类型:
- LCA: Karat 002 Longest Common Continuous Subarray res = history1[i - dp[i][j]:i]
- LIS:
LeetCode 300 Longest Increasing Subsequence
LeetCode 368 Largest Divisible Subset - LCS: LeetCode 1143 Longest Common Subsequence
LIS过程:
- path跟dp数组一样大,记录回溯跳转j -> i路径path[i - 1] = j - 1
- 记录回溯开始点if res == dp[i]: biggest_pos = i - 1
- 循环长度为res, 从biggest_pos开始
Python代码:
1 | def print_path(self, nums, path[:biggest_pos + 1], dp_value = res): |
LCS:
- path和dp一样,不用特别数组
- 从右下到左上,若上或左值一样,向此方向移动,直到左和上少一,此时res -= 1
Python代码:
1 | longest, res = dp[-1][-1], '' |