斐波那契型 一般是直观的状态转移 选或不选的问题 dp[i] 表示前i个最多能有多少钱 每个选还是不选?因为选了这个 就不能选下一个 由此得出状态转移方程 dp[i] = max(dp[i - 2] + cost[i], dp[i - 1]) 选了这个 就需要加上这个的值(并且不能选i - 1) 不选这个 就直接从i - 1转移
初始化 视情况而定 一般初始化 dp[0] 、dp[1] 等 前几个元素
1 2 3 4 5 6 7 8 9 10 11 class Solution : def rob (self, nums: List [int ] ) -> int : n = len (nums) if n < 3 : return max (nums) dp = [0 ] * (n + 1 ) dp[0 ] = nums[0 ] dp[1 ] = max (nums[0 ], nums[1 ]) for i in range (2 , n): dp[i] = max (dp[i - 1 ], dp[i - 2 ] + nums[i]) return dp[n - 1 ]
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def deleteAndEarn (self, nums: List [int ] ) -> int : n = max (nums) cost = [0 ] * (n + 1 ) for i, x in enumerate (nums): cost[x] += x dp = [0 ] * (n + 1 ) dp[0 ] = cost[0 ] dp[1 ] = cost[1 ] for i in range (2 , n + 1 ): dp[i] = max (dp[i - 2 ] + cost[i], dp[i - 1 ]) return dp[n]
矩阵型 一般是类似于走迷宫 用二维的 dp[i] [j] i,j表示坐标 状态从上一坐标 转移而来 一般是相邻的位置 比如i - 1 j - 1 等
注意 这类问题 一般需要判断该位置是否已经走过? 比如求最大值可以设初值为0 判断为0即未访问 求最小值设为inf等
得到状态转移方程为 dp[i] [j] = max(dp[i - 1] [j], dp[i] [j - 1]) + cost[]
初始化一般是用于转移的前几个数
由于数组的原因 下标经常从1开始 到n+1结束 相应的cost数组的下标也需要改变
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def uniquePaths (self, m: int , n: int ) -> int : if n == 1 or m == 1 : return 1 dp = [[0 ] * (n + 1 ) for _ in range (m + 1 )] dp[1 ][1 ] = 0 dp[1 ][2 ] = dp[2 ][1 ] = 1 for i in range (m): for j in range (n): if dp[i][j] == 0 : dp[i][j] = dp[i - 1 ][j] + dp[i][j - 1 ] return dp[m][n]
1 2 3 4 5 6 7 8 9 10 11 class Solution : def minPathSum (self, grid: List [List [int ]] ) -> int : m = len (grid) n = len (grid[0 ]) dp = [[10 ** 9 ] * (n + 1 ) for i in range (m + 1 )] dp[1 ][1 ] = grid[0 ][0 ] for i in range (1 , m + 1 ): for j in range (1 , n + 1 ): if dp[i][j] == 10 ** 9 : dp[i][j] = min (dp[i - 1 ][j], dp[i][j - 1 ]) + grid[i - 1 ][j - 1 ] return dp[m][n]
这题同样是矩阵型
dp[i] [j] 表示以i j 为右下角的最大正方形的 边长 每次看他的上、左和上左 三个格子的最大边长 + 1
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def maximalSquare (self, matrix: List [List [str ]] ) -> int : m = len (matrix) n = len (matrix[0 ]) dp = [[0 ] * (n + 1 ) for _ in range (m + 1 )] for i in range (m): for j in range (n): if matrix[i - 1 ][j - 1 ] == '1' : dp[i][j] = min (dp[i - 1 ][j], dp[i][j - 1 ], dp[i - 1 ][j - 1 ]) + 1 ret = max (ret, dp[i][j]) return ret ** 2
想到 从第一行开始取数 然后向下转移
比如g[0] [0] 就可以到 g[1] [0]….g[1] [m - 1]这样
于是用dfs(i, j) 来表示状态 在第i行第j列的最小值
然后就要考虑怎么递增 从i j 递增到下一行的第k个 那么下一行第k个的值 就是dfs(i + 1, k) + moveCost[grid[i] [j]] [k]
然后考虑退出条件 不断向下 那肯定就是到第n - 1行结束
结束的时候要返回什么? 这和存的东西 有关 dfs(i, j)表示i j 的最小值 然后往下转移 最后一行不再往下转移 最小值就是它本身
然后通过这个转移的过程 可以发现 i j处的最小值 其实是i j 加上 从i j往下 的所有的(也就是 我们最后会把所有的都加到第0行 返回第0行的最小值)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution : def minPathCost (self, grid: List [List [int ]], moveCost: List [List [int ]] ) -> int : n = len (grid) m = len (grid[0 ]) def dfs (i, j ): if i == n - 1 : return grid[i][j] ret = inf for k, c in enumerate (moveCost): res = min (res, dfs(i + 1 , k) + c) return res + grid[i][j] return min (dfs(0 , j) for j in range (m))
然后翻译成递推
dfs改dp
递归改循环
递归边界改为dp初始值
递归入口即为答案
dfs(i, j) -> dp[i] [j] 几个参数 就是几维
dp[-1] = grid[-1] 递归的边界 最后一行 就是初始值 因为我们算i j 的时候 其实是必须先算出来 i + 1 k 的值 然后再往上转移的
所以i要从下往上遍历 最后dp[0] 的最小值就是答案
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def minPathCost (self, grid: List [List [int ]], moveCost: List [List [int ]] ) -> int : n = len (grid) m = len (grid[0 ]) dp = [[inf] * m for _ in range (n)] dp[-1 ] = grid[-1 ] for i in range (n - 2 , -1 , -1 ): for j in range (m): for k, c in enumerate (moveCost): dp[i][j] = min (dp[i + 1 ][k] + c, dp[i][j]) dp[i][j] += grid[i][j] return min (dp[0 ])
这个题目与上一个题有点像 都是矩阵型 状态转移方程也是由左(j - 1)上(i - 1)转移到[i, j]
四维DP 状态表示 dp[i] [j] [cnt] [d] 表示i到j中 拿到cnt个物品且最大价值为d的情况
状态计算 有选或不选两种 (只有当拿的东西不多于k并且价值大于当前最大价值才可以拿)
不选就直接转移 dp[i] [j] [c] [d] = (dp[i] [j] [c] [d] + dp[i - 1] [j] [c] [d]) % mod;
选的话 需要从c - 1转移过来 并且要加上所有价值小于d的情况
最后需要加上所有的最大价值的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <iostream> using namespace std;int row, col, k;const int N = 55 , mod = 1e9 +7 , M = 15 ;int nums[N][N], dp[N][N][M][M];int main () { cin>>row>>col>>k; for (int i = 1 ; i <= row; i++) { for (int j = 1 ; j <= col; j++) { cin>>nums[i][j]; nums[i][j]++; } } dp[1 ][1 ][0 ][0 ] = 1 ; dp[1 ][1 ][1 ][nums[1 ][1 ]] = 1 ; for (int i = 1 ; i <= row; i++) { for (int j = 1 ; j <= col; j++) { for (int c = 0 ; c <= k; c++) { for (int d = 0 ; d < M; d++) { dp[i][j][c][d] = (dp[i][j][c][d] + dp[i - 1 ][j][c][d]) % mod; dp[i][j][c][d] = (dp[i][j][c][d] + dp[i][j - 1 ][c][d]) % mod; if (c > 0 && d == nums[i][j]) { for (int s = 0 ; s < d; s++) { dp[i][j][c][d] = (dp[i][j][c][d] + dp[i - 1 ][j][c - 1 ][s]) % mod; dp[i][j][c][d] = (dp[i][j][c][d] + dp[i][j - 1 ][c - 1 ][s]) % mod; } } } } } } int res = 0 ; for (int i = 0 ;i < M; i++) { res = (res + dp[row][col][k][i])%mod; } cout<<res; }
单字符串型 在单个字符串上进行操作 一般状态表示也是两个变量 i, j 然后取出一个区间 j 一般表示端点 s[i:j]或s[j: i]
初始化 一般需要初始化大量的字符串 比如 回文串 需要初始化每个s[i:i] 为1
dp[i] [j] 表示i到j是否为回文串 状态转移为 看12…..21 比如 1 == 1 那就看前面 一段2…..2是不是回文
如果1 != 1 那就直接false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution : def longestPalindrome (self, s: str ) -> str : n = len (s) if n < 2 : return s begin = 0 max_len = 1 dp = [[False ] * n for _ in range (n)] for i in range (n): dp[i][i] = True for L in range (2 , n + 1 ): for i in range (n): j = i + L - 1 if j >= n: break if s[i] != s[j]: dp[i][j] = False else : if L < 3 : dp[i][j] = True else : dp[i][j] = dp[i + 1 ][j - 1 ] if dp[i][j] and j - i + 1 > max_len: max_len = j - i + 1 begin = i return s[begin:begin + max_len]
跟前一题很像 用dp[i]表示 前i个能不能被word拼接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution : def wordBreak (self, s: str , wordDict: List [str ] ) -> bool : n = len (s) dp = [False ] * (n + 1 ) for i in range (1 , n + 1 ): for j in range (i): if dp[i] == True : continue if dp[j] == False : continue if s[j:i] in wordDict: s[i] = True break return dp[-1 ]
子序列问题 不连续 一般就要用i 和 j
dp[i] [j] 表示i 到j 最长是多少
当s[i] == s[j]的时候 dp[i] [j] 就可以从dp[i + 1] [j - 1]更新来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Solution : def longestPalindromeSubseq (self, s: str ) -> int : n = len (s) dp = [[0 ] * (n + 1 ) for _ in range (n + 1 )] for i in range (n): dp[i][i] = 1 ret = 0 for i in range (n - 1 , -1 , -1 ): for j in range (i + 1 ): if s[i] == s[j]: dp[i][j] = dp[i + 1 ][j - 1 ] + 2 else : dp[i][j] = max (dp[i + 1 ][j], dp[i][j - 1 ]) ret = max (ret, dp[i][j]) return ret
双字符串型 双字符串的题一般要用dp[i] [j] 一个用来表示第一个字符串的 第i个 一个用来表示第2个字符串的前j个
dp[i] [j] 表示第一个到i 第二个到j 相同的最小编辑次数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution : def minDistance (self, word1: str , word2: str ) -> int : """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""" n, m = len (word1), len (word2) dp = [[0 ] * (m + 1 ) for _ in range (n + 1 )] dp[0 ] = list (range (m + 1 )) for i in range (n): dp[i + 1 ][0 ] = i + 1 for j in range (m): if word1[i] == word2[j]: dp[i + 1 ][j + 1 ] = dp[i][j] else : dp[i + 1 ][j + 1 ] = min (dp[i][j], dp[i][j + 1 ], dp[i + 1 ][j]) + 1 return dp[n][m]
这题与上一题不同 在于 上一题是修改s使其与 t相等 所以 不相等的时候只需要修改 就可以相等 然后位移指针
但是这个题 是求t在x中出现的次数 也就是说t 必须全部匹配 所以t的指针j 只有在匹配的时候才能位移
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution : def numDistinct (self, s: str , t: str ) -> int : """def dfs(i, j): if i < 0: return 0 if j < 0: return 1 # 表示已经匹配 if s[i] == t[j]: return dfs(i - 1, j - 1) + dfs(i - 1, j) # 选或不选两种情况 return dfs(i - 1, j) """ n, m = len (s), len (t) dp = [[0 ] * (m + 1 ) for _ in range (n + 1 )] for i in range (n): for j in range (m): if j == 0 : dp[i][0 ] = 1 elif s[i] == t[j]: dp[i][j] = dp[i - 1 ][j - 1 ] + dp[i - 1 ][j] else : dp[i][j] = dp[i - 1 ][j] return dp[n][m]
由于直接求最小不好求 可以选择 先求出两个字符串的ascll总和 然后求最长子序列 最后再减去最长子序列的ascll * 2 就是删除的最小和
dp[i] [j] 表示最长子序列的ascll和 (即 到s1[i] s2[j]的ascll和)
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def minimumDeleteSum (self, s1: str , s2: str ) -> int : n, m = len (s1), len (s2) dp = [[0 ] * (m + 1 ) for _ in range (n + 1 )] s = sum ([ord (s1[i]) for i in range (n)]) s += sum ([ord (s2[i]) for i in range (m)]) for i in range (n): for j in range (m): if s1[i] == s2[j]: dp[i + 1 ][j + 1 ] = dp[i][j] + ord (s1[i]) else : dp[i + 1 ][j + 1 ] = max (dp[i + 1 ][j], dp[i][j + 1 ]) return s - dp[n][m] * 2
先是找最长的递增子序列 用maxn记录最长的长度 dp[i] 记录到i的最长子序列长度 (最长递增子序列)
由于本题求得是个数 需要增加cnt[i] 表示 到i 的最长递增子序列的个数
对于每个i 遍历0 ~ i的每个j 如果nums[i] > nums[j] 说明i 可以接在j后面形成递增子序列
如果dp[i] < dp[j] + 1那么说明到i的最长递增子序列的长度需要更新 那么到i 的最长子序列的数量 可以直接更新(到j的数量 直接转移到 到i的数量 )
如果dp[i] == dp[j] + 1 那么说明到i这个地方 他的最长递增子序列(并非整个数组最长而是0~i最长)的数量+= 到j的数量
最后把到每个i 的等于maxn的数量加起来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution : def findNumberOfLIS (self, nums: List [int ] ) -> int : n = len (nums) dp = [0 ] * (n + 1 ) cnt = [0 ] * (n + 1 ) maxn = -1 for i in range (n): dp[i] = 1 cnt[i] = 1 for j in range (i): if nums[i] > nums[j]: if dp[i] < dp[j] + 1 : dp[i] = dp[j] + 1 cnt[i] = cnt[j] elif dp[i] == dp[j] + 1 : cnt[i] += cnt[j] maxn = max (maxn, dp[i]) ret = 0 for i in range (n): if dp[i] == maxn: ret += cnt[i] return ret
模板题 最长上升子序列
状态表示dp[i] 表示到i 到j 的最长链
状态转移 i + 1的l如果大于i的r
1 2 3 4 5 6 7 8 9 10 11 12 class Solution : def findLongestChain (self, pairs: List [List [int ]] ) -> int : n = len (pairs) pairs.sort(key=lambda x: (x[0 ], x[1 ])) dp = [0 ] * (n + 1 ) for i in range (n): dp[i] = 1 for j in range (n): if pairs[j][1 ] < pairs[i][0 ]: dp[i] = max (dp[j] + 1 , dp[i]) return max (dp)
公共子序列型 一般需要二维 i用来表示到text1 j表示到text2 的时候最长公共子序列为多长
更新状态一般是当text1[i - 1] == text2[i - 1]的时候就是dp[i] [j] = dp[i - 1] [j - 1] + 1 意思是到i j 公共子序列长度增加
否则就是选出dp[i - 1] [j] 和dp[i] [j - 1] 中较大的
dp[i] [j]
1 2 3 4 5 6 7 8 9 10 11 class Solution : def longestCommonSubsequence (self, text1: str , text2: str ) -> int : m, n = len (text1), len (text2) dp = [[0 ] * (m + 1 ) for _ in range (n + 1 )] for i in range (1 , m + 1 ): for j in range (1 , n + 1 ): if text1[i - 1 ] == text2[j - 1 ]: dp[i][j] = dp[i - 1 ][j - 1 ] + 1 else : dp[i][j] = max (dp[i - 1 ][j], dp[i][j - 1 ]) return dp[m][n]
类似最长子串的问题 判断条件变为 连续数字
判断连续的数字 就用i - j == max - min 从i到j中的最大值与最小值的差等于i - j的话 那么这个范围内的数字就是连续的
dp[i] 表示前i个数共有多少种表示方法
二重循环从i到0 倒着找 如果找到了一个范围j~i使得里面的数字连续的话 (原来前i个数 共有dp[i] 种表示方法 前j - 1个数共有dp[j - 1]种) 那么现在dp[i] = dp[i] + dp[j - 1] 因为前j - 1的方案数 现在j ~i也可以了 就可以接上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <iostream> using namespace std;int a[100010 ], dp[100010 ];int mod = 1e9 + 7 ;int main () { int n; cin>>n; for (int i = 1 ; i <= n; i++) { cin>>a[i]; } dp[0 ] = 1 ; for (int i = 1 ; i <= n; i++) { int ma = a[i], mi = a[i]; for (int j = i; j > 0 ; j--) { if (i - j == ma - mi) { dp[i] = (dp[i] + dp[j - 1 ]) % mod; } } } cout<<dp[n]; return }
最长公共子序列 同时要满足线不能相交(也就是要有序 )其实跟最长公共子序列一样
也是最长公共子序列 只不过不是两个字符串 而是一个字符串 从前往后 一个字符串从后往前 来比较
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution : def minInsertions (self, s: str ) -> int : n = len (s) dp = [[0 ] * (n + 1 ) for _ in range (n + 1 )] for i in range (n): dp[i][i] = 1 for i in range (n - 1 , -1 , -1 ): for j in range (i + 1 , n): if s[i] == s[j]: dp[i][j] = dp[i + 1 ][j - 1 ] + 2 else : dp[i][j] = max (dp[i + 1 ][j], dp[i][j - 1 ]) return n - dp[0 ][n - 1 ]
树上DP 求二叉搜索树的种数 就是G[0] = 1 G[1] = 1 G[2] = 2
卡特兰数
1 2 3 4 5 6 7 8 9 class Solution : def numTrees (self, n: int ) -> int : g = [0 ] * (n + 1 ) g[0 ] = 1 g[1 ] = 1 for i in range (2 , n + 1 ): for j in range (1 , i + 1 ): g[i] += g[j - 1 ] * g[i - j] return g[n]
搜索 首先想要递归什么?递归start和end 就是每棵树包含start到end的元素
递归出口条件 就是当start < end 的时候 说明没有元素 return None
然后思考如何去递归 既然是递归start和end中间的元素 那么就可以想到 左子树是start到i 右子树是i+1到end 然后再去把这两个树加上根节点拼起来 存入答案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution : def generateTrees (self, n: int ) -> List [Optional [TreeNode]]: def generate (start, end ): if start > end: return [None , ] alltrees = [] for i in range (start, end + 1 ): leftTree = generate(start, i) rightTree = generate(i + 1 , end) for l in leftTree: for r in rightTree: curTree = TreeNode(i) curTree.left = l curTree.right = r allTree.append(curTree) return allTrees return generate(1 , n) if n else []
首先思考怎么表示状态 偷或不偷 返回值表示 左边偷的钱 和右边偷的钱 如果左边或右边偷了 那么根就不能偷 否则偷根
递归出口 当root is None的时候 就返回0,0 表示
然后是每个 当dfs(root.left) 表示偷左不偷右 right同理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution : def rob (self, root: Optional [TreeNode] ) -> int : def dfs (root ): if root is None : return 0 , 0 l_rob, r_not_rob = dfs(root.left) l_not_rob, r_rob = dfs(root.right) rob = root.val + l_not_rob + r_not_rob not_rob = max (l_rob, l_not_rob) + max (r_rob, r_not_rob) return rob, not_rob return max (dfs(root))
首先思考递归表示 root
然后是递归出口 当root is None的时候 返回0
然后是算出左边的结点 右边的结点 再加上中间的 求最大值
返回最大值 注意最后的返回值要和0比较 因为有负数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution : def maxPathSum (self, root: Optional [TreeNode] ) -> int : ans = -inf def dfs (root ): if root is None : return 0 l_val = dfs(root.left) r_val = dfs(root.right) nonlocal ans ans = max (l_val + r_val + root.val, ans) return max (max (l_val, r_val) + root.val, 0 ) dfs(root) return ans
背包 分组背包 总和为n 每个骰子只能有一个点数 选哪个的问题
并且不是分组背包求最大值的问题 而是分组背包求固定和的问题 用的转移方程是 j - ds
第三重循环枚举点数 即 从dp[i - 1] 转移到dp[i] 的情况 有dp[i - 1] [j - 1] + 1;dp[i - 1] [j - 2] + 2 ……dp[i - 1] [j - k] + k
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution : def numRollsToTarget (self, n: int , k: int , target: int ) -> int : mod = 10 ** 9 + 7 dp = [[0 ] * (max (target + 1 , n * k + 1 )) for _ in range (n + 1 )] for i in range (1 , min (target, k) + 1 ): dp[1 ][i] = 1 maxn = n * k for i in range (2 , n + 1 ): for j in range (i, maxn + 1 ): ds = 1 while j - ds >= 0 and ds <= k: dp[i][j] += dp[i - 1 ][j - ds] dp[i][j] %= mod ds += 1 return dp[n][target]
组合型背包 这个题目与上题有区别 一个是求排列数 一个是求组合数
也就是说 上一题会把1 2 和2 1当成两种情况 但是这一题 视为同一种情况 如果按上面的写法 最后结果会偏大
本质原因是子问题的改变 我们需要加上对硬币的限制 1 2 和 2 1 是同种情况 因此可以枚举硬币而不是枚举总和 从小到大枚举硬币 就不会出现2 1这种情况
现在子问题变为 到第k个硬币 合成金额i的组合数
状态转移为dp[k] [i] = dp[k - 1] [i] + dp[k] [i - coins[k]] (i > coins[k])
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 for (int i = 0 ; i < coins.size (); i++) { dp[i][0 ] = 1 ; }for (int k = 0 ; k < coins.size (); k++) { for (int i = 1 ; i <= amount; i++) { if (i >= coins[k - 1 ]) dp[k][i] = dp[k][i - coins[k - 1 ]] + dp[k - 1 ][i]; else { dp[k][i] = dp[k - 1 ][i]; } } }
此时就算交换两个for循环的顺序也不会影响结果 因为交换后子数组的状态转移方程不变 得到的结果也不变
如果要降成一维的数组 重新定义子问题为 必须选择第k个硬币的时候 凑成金额i的方案数 不能交换for循环的顺序 因为交换完之后 子问题的意义就是 对于金额i 有几种选择硬币的方案数(理解两个子问题的差别 一个是可重复一个不行)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution {public : int change (int amount, vector<int >& coins) { vector<int >dp (amount + 1 , 0 ); dp[0 ] = 1 ; for (auto x: coins) { for (int i = 0 ; i <= amount; i++) { if (i >= x) dp[i] += dp[i - x]; } } for (int i = 0 ; i <= amount; i++) { cout<<dp[i]<<" " ; } return dp[amount]; } };
排列型背包 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution {public : int coinChange (vector<int >& coins, int amount) { if (amount == 0 )return 0 ; int n = coins.size (); vector<int >dp (amount + 1 , 1e9 ); for (auto x:coins) { if (x <= amount) dp[x] = 1 ; } for (int i = 0 ; i <= amount; i++) { for (auto x: coins) { if (i >= x) dp[i] = min (dp[i - x] + 1 , dp[i]); } } return dp[amount] == 1e9 ? -1 : dp[amount]; } };
背包问题 i 表示到i要多少数
当i > j ^2 的时候i 就可以更新 找到最少的次数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution : def numSquares (self, n: int ) -> int : num = 0 for num in range (n): if num ** 2 > n: break elif num ** 2 == n: return 1 dp = [0 ] * (n + 1 ) for i in range (1 , n + 1 ): minn = inf j = 1 while j ** 2 <= i: minn = min (minn, dp[i - j ** 2 ]) j += 1 dp[i] = minn + 1 return dp[n]
二维 k 表示前k个硬币 i 表示凑成i 元
前k个硬币凑成i元的种类为 前k - 1个凑成i 元 加上 前k个硬币凑成了i - k元 就差k了
1 2 3 4 5 6 7 8 9 10 11 class Solution : def change (self, amount: int , coins: List [int ] ) -> int : n = len (coins) dp = [[0 ] * (amount + 1 ) for _ in range (n + 1 )] for k in range (n + 1 ): dp[k][0 ] = 1 for k in range (1 , n + 1 ): for i in range (1 , amount + 1 ): if i >= coins[k - 1 ]: dp[k][i] = dp[k][i - coins[k - 1 ]] + dp[k - 1 ][i]
定差型 不是 i - 1 和i 的关系 而是 每次比较 i 和i - nums[i] 这样的 某个差值nums[i] 之间的关系
每次到达dp[i] 的时候 就去 dp[i - difference] 找上一个相差difference的
1 2 3 4 5 6 7 8 class Solution : def longestSubsequence (self, arr: List [int ], difference: int ) -> int : dp = defaultdict(int ) for x in arr: dp[x] = dp[x - difference] + 1 return max (dp.values())
与上题相同 但是difference需要自己找
每个dp[i] 存储一个哈希表
每个哈希表存储 i - 1 ~ 0 的所有数 和nums[i] 的差值(key)和这个差值d 的出现的次数
比如9 4 7 2 10 到10 的时候 就往前找 2与10 差8 公差为8 的就是1个 再在2中找 公差为8的次数 0 +1=1
7 与10 差3 公差为3 在7中找公差为3的 为1 (7 和4) 总次数为2
最后个数是次数+1
1 2 3 4 5 6 7 8 9 10 class Solution : def longestArithSeqLength (self, nums: List [int ] ) -> int : dp = [{} for i in range (n)] for i in range (n): for j in range (i - 1 , -1 , -1 ): d = nums[i] - nums[j] if d not in dp[i]: dp[i][d] = dp[j].get(d, 1 ) + 1 return max (max (d.values()) for d in dp[1 :])
从n向前寻找 当i + nums[i] <= n的时候 是可以形成 [长度+数组]的形式的 这时候就要检查这个地方是否需要选(可以选 也可以不选)
就要判断选这个点的代价(因为选这个可能影响其他代价更小的点)选了就是 dp[i + nums[i] + 1] 就是 这个点清除掉之后 比如
1 2 1 2 3 4 5 6 7 3 3 4 5 2 6 1
选了nums[1]的话 从1到n的代价就为 nums[5]的代价 因为1~4都被消除了
如果不选i 的话 那就是dp[i + 1] + 1(即直接删除这个点)
如果i + nums[i] > n 不能形成 数组 那就需要删除 删除的话就是dp[i] = dp[i + 1] + 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 #include <bits/stdc++.h> using namespace std;using ll = long long ;using PII = pair<int , int >;#define pb push_back #define all(x) (x).begin(), (x).end() #define fi first #define se second #define endl '\n' #define debug(x) \ { \ cerr << #x << " = " << x << endl; \ } #define debugfull(x) \ { \ cerr << #x << " = " << x << " (line " << __LINE__ << ")" << endl; \ } const int N = 2e5 + 10 ;void solve () { int n; cin >> n; vector<int > nums (N) , dp (n + 5 , 0 ) ; for (int i = 1 ; i <= n; i++) { cin >> nums[i]; } dp[n] = 1 ; for (int i = n - 1 ; i > 0 ; i--) { if (nums[i] + i > n) dp[i] = dp[i + 1 ] + 1 ; else dp[i] = min (dp[i + 1 ] + 1 , dp[i + nums[i] + 1 ]); } cout << dp[1 ] << endl; }int main () { ios::sync_with_stdio (0 ); cin.tie (0 ); int T; cin >> T; while (T--) { solve (); } return 0 ; }
跟上面那题有点像。。但是也不一样
找买水果的最少的钱数 首先写递归
这个水果买或不买 怎么看 哪些水果是免费的? 从1开始递归往后找
能够不花钱的水果就是i + 1, i + 2 …, 2 * i
那我下一个要买的水果 就是从i + 1 i + 2… 2 * i + 1 因为2 * i + 1必定是要买的 而前面i个是可以选择买的 那么下一个买的肯定就在这中间
那么dfs(i)的意义就是 从1到后面需要花的最少的钱
那么从i开始 后面要花的最少的钱 就是 我现在买了i的钱 再加上下一个要买的 j 的最小值 (从j 到后面需要花的最少的钱)就是 i买的覆盖了i到2 * i 然后加上 dfs(j) 的最小值就是dfs(i)
然后递归出口 大于n的时候就是 0 就是不用在花钱了
细节地方是 prices 下标从1开始 我们dfs也定义从1开始 但是本质上prices是个数组 下标从0开始 所以每次加的时候 加prices[i - 1]
1 2 3 4 5 6 7 8 9 10 class Solution : def minimumCoins (self, prices: List [int ] ) -> int : n = len (prices) @cache def dfs (i ): if i > n: return 0 res = min (dfs(j) for j in range (i + 1 , 2 * i + 2 )) return res + prices[i - 1 ] return dfs(1 )
优化可以是 2 * i > n的时候 return prices[i - 1] 因为这时候只用花一次钱就行了
1 2 3 4 5 6 7 8 9 10 class Solution : def minimumCoins (self, prices: List [int ] ) -> int : n = len (prices) @cache def dfs (i ): if 2 * i >= n: return prices[i - 1 ] res = min (dfs(j) for j in range (i + 1 , 2 * i + 2 )) return res + prices[i - 1 ] return dfs(1 )
然后翻译成递推 因为我们是从1开始到n的 那么变成递推就要改成从n 开始 因为从1 开始 递归到n 说明我实际上是从n算到1的 就是 我算前面的时候 后面是已经先算出来的
1 2 3 4 5 6 7 8 9 10 class Solution : def minimumCoins (self, prices: List [int ] ) -> int : n = len (prices) f = [0 ] * (n + 1 ) for i in range (n, 0 , -1 ): if 2 * i >= n: f[i] = prices[i - 1 ] else : f[i] = prices[i - 1 ] + min (f[i + 1 : 2 * i + 2 ]) return f[1 ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <iostream> using namespace std;int n, s, a, b;const int mod = 100000007 ;int f[1010 ][1010 ];int getmod (int x, int y) { return (x % y + y) % y; }int main () { cin>>n>>s>>a>>b; f[0 ][0 ] = 1 ; for (int i = 1 ; i < n; i++) { for (int j = 0 ; j < n; j++) { f[i][j] = ((f[i - 1 ][getmod (j - a * (n - i), n)] + f[i - 1 ][getmod (j + b * (n - i), n)]))%mod; } } cout<<f[n - 1 ][getmod (s, n)]; }
[d]: