动态规划问题的一般形式就是求最值
求解动态规划的核心问题是穷举
把大的问题细化到一个点,先研究在这个小的点上如何解决问题,然后再通过递归/迭代的方式扩展到整个问题
定义 dp 数组/函数的含义 -> 明确 base case -> 明确「状态」-> 明确「选择」
可以用数学归纳的思想来简单理解动态规划:
子序列问题
最长递增子序列
给你一个整数数组
nums,找到其中最长严格递增子序列的长度。子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,
[3,6,2,7]是数组[0,3,1,6,2,2,7]的子序列
定义:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度
base case:dp[i] 初始值为 1,
dp[i]怎么推出dp[i+1]:比num[i]小的子序列加1中最长的
完整代码
最大子数组和
给你一个整数数组
nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。
定义:nums[0..i] 中的「最大的子数组和」为 dp[i]
这样是不行的,无法从dp[i]推出dp[i+1],因为不确定nums[0..i] 中的最大子数组与num[i+1]相邻
定义:以 nums[i] 为结尾的「最大子数组和」为 dp[i]
base case:dp[0] = num[0]
dp[i]怎么推出dp[i+1]:dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]);
完整代码
最长公共子序列
给定两个字符串
text1和text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回0。
[!IMPORTANT]
对于两个字符串求子序列的问题,都是用两个指针
i和j分别在两个字符串上移动,大概率是动态规划思路
定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i] [j]
base case: dp[0] [..] = dp[..] [0] = 0
怎么得到dp[i] [j]:
s1[i-1] == s2[j-1] , dp[i] [j] = 1 + dp[i - 1] [j - 1];
s1[i-1] != s2[j-1] , dp[i] [j] = Math.max(dp[i] [j - 1], dp[i - 1] [j]);
完整代码
编辑距离
给你两个单词
word1和word2, 请返回将word1转换成word2所使用的最少操作数 。你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
定义:s1[0..i-1] 和 s2[0..j-1] 的编辑距离为 dp[i] [j]
base case: dp[0] [..] = dp[..] [0] = 0
怎么得到dp[i] [j]:
s1[i-1] == s2[j-1] , dp[i] [j] = dp[i - 1] [j - 1];
s1[i-1] != s2[j-1] ,
- CodeBlock Loading...
完整代码
最长回文子序列
给你一个字符串
s,找出其中最长的回文子序列,并返回该序列的长度。子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
定义:在子串 s[i..j] 中,最长回文子序列的长度为 dp[i][j]
base case:dp[i] [j] = 1,dp[i > j] = 0
怎么得到dp[i] [j] :
完整代码
子序列问题总结
一个字符串常用一维dp数组:
比如 最长递增子序列 和 最大子数组和 都是这个思路
在这个思路中 dp 数组的定义是:
在子数组 arr[0..i] 中,以 arr[i] 结尾的子序列的长度是 dp[i]
两个字符串或者回文字符串常用二维dp数组: 比如 最长公共子序列 和 编辑距离 及 最长回文子序列
在子数组 arr1[0..i] 和子数组 arr2[0..j] 中,我们要求的子序列长度为 dp[i][j]
在子数组 array[i..j] 中,我们要求的子序列的长度为 dp[i][j]
背包类型问题
0-1背包问题
输入:
CodeBlock Loading...输出最大价值
定义: 对于前 i 个物品,当前背包的容量为 w,这种情况下可以装的最大价值是 dp[i][w]
base case: dp[0] [..] = dp[..] [0] = 0
怎么推出dp [i] [w] :
完整代码
子集背包问题
给你一个 只包含正整数 的 非空 数组
nums。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
先对集合求和,得出 sum,把问题转化为背包问题:
给一个可装载重量为 sum / 2 的背包和 N 个物品,每个物品的重量为 nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满?
定义:dp[i] [j] 表示,对于前 i 个物品,当前背包的容量为 j 时,若值为 true,则说明可以恰好将背包装满
base case: dp[..] [0] = true , dp[0] [..] = false
怎么得到dp[i] [j] : dp[i] [j] = dp[i - 1] [j] || dp[i - 1] [j - nums[i - 1]]; 装或者不装有一个为true即为true
完整代码
给你一个非负整数数组
nums和一个整数target。向数组中的每个整数前添加
'+'或'-',然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1],可以在2之前添加'+',在1之前添加'-',然后串联起来得到表达式"+2-1"。返回可以通过上述方法构造的、运算结果等于
target的不同 表达式 的数目。
可以推出 sum(A) = (target + sum(nums)) / 2,也就是把原问题转化成:nums 中存在几个子集 A,使得 A 中元素的和为 (target + sum(nums)) / 2
定义: dp[i][j] = x 表示,若只在前 i 个物品中选择,若当前背包的容量为 j,则最多有 x 种方法可以恰好装满背包
base case: dp[0] [..] = 0 , dp[0] [0] = 1 ,dp[1..n] [0] 不一定就一种
怎么推出dp[i] [j] : dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];
完整代码
完全背包问题
给你一个整数数组
coins表示不同面额的硬币,另给一个整数amount表示总金额。请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回
0。假设每一种面额的硬币有无限个。
背包问题的描述形式:
有一个背包,最大容量为 amount,有一系列物品 coins,每个物品的重量为 coins[i],每个物品的数量无限。请问有多少种方法,能够把背包恰好装满?
定义: dp[i][j] :若只使用前 i 个物品(可以重复使用),当背包容量为 j 时,有 dp[i][j] 种方法可以装满背包
base case : dp[0][..] = 0, dp[..][0] = 1
怎么推出dp[i] [j] : dp[i][j] = dp[i - 1][j] + dp[i][j-coins[i-1]];
完整代码
打家劫舍问题
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
定义: dp[i] = x 表示:从第 i 间房子开始抢劫,最多能抢到的钱为 x
base case : dp [n], dp [n+1] 为 0 没有房间可以抢劫
怎么推出dp [i] : dp[i] = Math.max(dp[i + 1], nums[i] + dp[i + 2]); 抢 或者 不抢
完整代码:
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
同打家劫舍,因为环形,相当于nums[0...n-2] 和 nums[1...n-1] 即不抢第一家和不抢最后一家的最大值
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为
root。除了
root之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。给定二叉树的
root。返回 *在不触动警报的情况下 ,小偷能够盗取的最高金额* 。
定义: dp(node) 抢这个节点
base case : 节点为null ,抢的为0
怎么推: 抢这个节点和不抢这个节点的最大值 res = Math.max(root.val + not, res); not 为抢下下层节点的值的和