一:动态规划原理
1、基本思想
问题的 最优解如果可以由子问题的最优解推导而出,则可以先求解子问题的最优解,再构造原问题的最优解;若子问题有较多的重复出现,则可以自底向上的从最终子问题向原问题逐步求解。
2、使用条件
可分为多个相关子问题,子问题的解被重复使用
1)优化子结构
- 一个问题的优化接包含了子问题的优化解
- 缩小子问题集合,只需那些优化问题中包含的子问题,降低实现复杂性
- 可以自下而上
2)重叠子问题
- 在问题的求解过程中,很多子问题的解将被重复使用
3、动态规划五部曲
-
确定dp数组(dp table)以及下标的含义
-
确定递推公式
-
dp数组如何初始化
-
确定遍历顺序
确定遍历顺序可以通过递推公式,
dp[i][j]
由哪个方向的dp
推出 -
举例推导dp数组
4、思考问题
这道题⽬我举例推导状态转移公式了么?
我打印dp数组的⽇志了么?
打印出来了dp数组和我想的⼀样么?
5、如何调试
(1)找问题的最好⽅式就是把dp数组打印出来
(2)状态转移在dp数组的上具体情况模拟⼀遍
二:背包问题
1、01背包详解
有N
件物品和⼀个最多能被重量为W 的背包。第i件物品的重量是weight[i]
,得到的价值是value[i]
。每件物品只能⽤⼀次,求解将哪些物品装⼊背包⾥物品价值总和最⼤。
1)二维数组
下标定义:
dp[i][j]
表示从下标[0-i]
的物品里任意取,放进容量为j
的背包,价值总和最大是多少。
先遍历背包或者先遍历物品都可以,因为dp[i-1][j]和dp[i - 1][j - weight[i]]
都在dp[i][j]
的左上⻆⽅向(包括正左和正上两个⽅向)
2)一维数组(滚动数组)
滚动数组:上一层可以重复利用,直接拷贝到当前层。
把dp[i-1]
那一层拷贝到dp[i]
上,表达式变成:dp[i][j]=max(dp[i][j],dp[i][j-weight[i]]+value[i])
转换成一维数组:dp[j]=max(dp[j],dp[j-weight[i]]+value[i])
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
一维数组是倒叙遍历,每次取得状态不会和之前取得状态重合,这样每种物品就只取⼀次了。
2、完全背包详解
有N件物品和⼀个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有⽆限个(也就是可以放⼊背包多次),求解将哪些物品装⼊背包⾥物品价值总和最⼤。
完全背包和01背包问题唯⼀不同的地⽅就是,每种物品有⽆限件
1)遍历顺序解析
我们知道01背包内嵌的循环是从⼤到⼩遍历,为了保证每个物品仅被添加⼀次。
⽽完全背包的物品是可以添加多次的,所以要从⼩到⼤去遍历,即:
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j < bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
在完全背包中,对于⼀维dp数组来说,其实两个for循环嵌套顺序同样⽆所谓!
因为dp[j]
是根据 下标j之前所对应的dp[j]
计算出来的。 只要保证下标j
之前的dp[j]
都是经过计算的就可以了
2)完全背包分类
弄清什么是组合,什么是排列很重要。
组合不强调顺序,(1,5)和(5,1)是同⼀个组合。
排列强调顺序,(1,5)和(5,1)是两个不同的排列。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
3、 解题思路
1)组合问题
组合问题公式
dp[i] += dp[i-num]
2)True、False问题
True、False问题公式
dp[i] = dp[i] or dp[i-num]
3)最大最小问题
最大最小问题公式
dp[i] = min(dp[i], dp[i-num]+1)或者dp[i] = max(dp[i], dp[i-num]+1)
以上三组公式是解决对应问题的核心公式。
4、解题步骤
0.动态规划三部曲
1.分析是否为背包问题。
2.是以上三种背包问题中的哪一种。
3.是0-1背包问题还是完全背包问题。也就是题目给的nums数组中的元素是否可以重复使用。
4.如果是组合问题,是否需要考虑元素之间的顺序。需要考虑顺序有顺序的解法,不需要考虑顺序又有对应的解法。
5、背包问题判定
背包问题具备的特征:给定一个target,target可以是数字也可以是字符串,再给定一个数组nums,nums中装的可能是数字,也可能是字符串,问:能否使用nums中的元素做各种排列组合得到target。
6、背包问题技巧
1.如果是0-1背包,即数组中的元素不可重复使用,nums放在外循环,target在内循环,且内循环倒序;
for num in nums:
for i in range(target, nums-1, -1):
2.如果是完全背包,即数组中的元素可重复使用,nums放在外循环,target在内循环。且内循环正序。
for num in nums:
for i in range(nums, target+1):
三:力扣题目解析
——基础题目
746. 使用最小花费爬楼梯
给你一个整数数组 cost
,其中 cost[i]
是从楼梯第 i
个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0
或下标为 1
的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
示例 1:
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。
1)题目描述
题⽬描述理解为:
每当你爬上⼀个阶梯你都要花费对应的体⼒值,⼀旦⽀付了相应的体⼒值,你就可以选择向上爬⼀个阶梯或者爬两个阶梯。
2)思路解析
-
确定dp数组(dp table)以及下标的含义
dp[i]
表示从楼梯第i
个台阶向上花费的最少体力 -
确定递推公式
可选择向上爬一个或者两个台阶:
dp[i-1] || dp[i-2]
花费最少体力:选择
min(dp[i-1],dp[i-2])
楼梯到达第
i
个台阶需要支付的费用:cost[i]
-
dp数组如何初始化
每当你爬上⼀个阶梯你都要花费对应的体⼒值
dp[0] = cost[0]
dp[1] = cost[1]
-
确定遍历顺序
从前到后遍历cost数组
-
举例推导dp数组
Math.min(dp[i-1],dp[i-2])+cost[i]
3)代码实现
class Solution {
public int minCostClimbingStairs(int[] cost) {
if(cost.length==2){
return Math.min(cost[0],cost[1]);
}
int[] dp = new int[cost.length];
dp[0] = cost[0];
dp[1] = cost[1];
for(int i=2;i<cost.length;i++){
dp[i] = Math.min(dp[i-1],dp[i-2])+cost[i];
}
return Math.min(dp[cost.length-1],dp[cost.length-2]);
}
}
62. 不同路径
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
![img](https://raw.githubusercontent.com/hyinyinh/picture/master/images/202203201255160.png)
输入:m = 3, n = 7
输出:28
1)思路解析
-
确定dp数组(dp table)以及下标的含义
dp[m][n]二维数组
下标的含义:到达
m-1,n-1
位置的路径数 -
确定递推公式
dp[i][j] = dp[i-1][j] + dp[i][j-1]
dp
只能从两个方向过来 -
dp数组如何初始化
由于机器人每次只能向下或者向右走一格,所以
dp[0][j] dp[i][0]
即第一行第一列初始化为1 -
确定遍历顺序
要确定
dp[i-1][j] dp[i][j-1]
都有值,所以从左到右一层一层遍历 -
举例推导dp数组
2)代码实现
class Solution {
public int uniquePaths(int m, int n) {
if(m==1 || n==1) return 1;
int[][] dp = new int[m][n];
for(int i=0;i<m;i++){
dp[i][0] = 1;
}
for(int j=0;j<n;j++){
dp[0][j] = 1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j] = dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
343. 整数拆分
给定一个正整数 n
,将其拆分为 k
个 正整数 的和( k >= 2
),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。
示例 1:
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
1)思路解析
-
确定dp数组(dp table)以及下标的含义
dp[j]
表示拆分j
得到的最大乘积dp[j]
-
确定递推公式
推导:
我们将原问题抽象为 f(n)
①那么f(n)
等价于max(1 * f(n - 1), 2 * f(n - 2), ..., (n - 1) * f(1))
。 ②数字
i
可以拆分成j + (i - j)
。但j * (i - j)
不一定是最大乘积,因为i-j
不一定大于dp[i - j]
(数字i-j拆分成整数之和的最大乘积),这 里要选择最大的值作为 dp[i] 的结果。 ③
dp[j]=Math.max(Math.max(dp[j-i]*j,(j-i)*j),dp[j])
-
dp数组如何初始化
0,1
无法拆分 所以从dp[2]
开始初始化dp[2]=1*1=1
-
确定遍历顺序
dp[j]=Math.max(Math.max(dp[j-i]*j,(j-i)*j),dp[j])
由推导公式可知
dp
必须从前往后遍历 因为从
dp[2]
初始化,为了确保数据一定有值,所以j(背包大小)
从3开始 物品
i
从1,开始j-i>2
-
举例推导dp数组
2)代码实现
class Solution {
public int integerBreak(int n) {
int[] dp = new int[n+1];
if(n==2) return 1;
if(n==3) return 2;
dp[2] = 2;
for(int i =3;i<=n;i++){
for(int j=1;j<i-1;j++){
dp[i] = Math.max(dp[i],Math.max(j *(i-j) , j*dp[i-j]));
}
}
return dp[n];
}
}
96. 不同的二叉搜索树
给你一个整数 n
,求恰由 n
个节点组成且节点值从 1
到 n
互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
示例 1:
输入:n = 3
输出:5
1)思路解析
-
确定dp数组(dp table)以及下标的含义
n有dp[n]
中不同的二叉搜索树 -
确定递推公式
有图可知推论:
当头结点为1时,二叉搜索树的布局和
n=2即dp[2]
时的布局一样(不考虑节点的值) 当头结点为2时,二叉搜索树的布局和
n=1即dp[1]
时的布局一样 当头结点为3时,二叉搜索树的布局和
n=2即dp[2]
时的布局一样 可知:
dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]
进一步得出:
dp[i] += dp[j - 1] * dp[i - j];
-
dp数组如何初始化
递推公式需要与
dp[0]
相乘,所以dp[0] = 1
-
确定遍历顺序
头结点
i
需要从1
开始遍历dp[i] += dp[j - 1] * dp[i - j];
可知i-j
必须大于0,所以j
从1开始遍历到i
-
举例推导dp数组
题目可知
2)代码实现
class Solution {
public int numTrees(int n) {
int[] dp = new int[n+1];
dp[0]=1;
dp[1]=1;
for(int i =2; i<=n;i++){
for(int j=1;j<=i;j++){
dp[i]+=dp[j-1] * dp[i-j];
}
}
return dp[n];
}
}
——01背包问题
外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历
for num in nums:
for i in range(target, nums-1, -1) :
416. 分割等和子集
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
1)题目解析
分割成等和子集,即分割成两份数值相等的集合,所以数组之和必须为偶数
2)思路解析
-
确定dp数组(dp table)以及下标的含义
将数组一分为二,求平均数是否能够填满
-
确定递推公式
j-nums[i]
减去当前数值还能存放的数值dp[j-nums[i]]+nums[i]
如果当前值存入dp[j]
当前值存入比之前的小所以保留之前的数 -
dp数组如何初始化
dp[j]=0
-
确定遍历顺序
由01背包问题可知
for(int i = 0; i < nums.size(); i++) { for(int j = target; j >= nums[i]; j--)
-
举例推导dp数组
3)代码实现
class Solution {
public boolean canPartition(int[] nums) {
int sum=0;
for(int i = 0;i<nums.length;i++){
sum+=nums[i];
}
if(sum%2!=0) return false;
int pack = sum/2;
int[] dp = new int[pack+1];
for(int i = 0;i<nums.length;i++){
for(int j=pack;j>=nums[i];j--){
dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
return dp[pack] == pack;
}
}
1049. 最后一块石头的重量 II
有一堆石头,用整数数组 stones
表示。其中 stones[i]
表示第 i
块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x
和 y
,且 x <= y
。那么粉碎的可能结果如下:
- 如果
x == y
,那么两块石头都会被完全粉碎; - 如果
x != y
,那么重量为x
的石头将会完全粉碎,而重量为y
的石头新重量为y-x
。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0
。
示例 1:
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
1)题目解析
该题目与上一题类似,都可以分成两块,填满平均值,将总数减去平均值乘以2即可得出剩下石头的最小可能重量
2)代码实现
class Solution {
public int lastStoneWeightII(int[] stones) {
int n = stones.length;
if(n==1) return stones[0];
if(n==2) return Math.abs(stones[0]-stones[1]);
int sum = 0;
for(int num : stones){
sum += num;
}
int mid = sum/2;
int[] dp = new int[mid+1];
for(int i=0;i<n;i++){//遍历物品
for(int j=mid;j>=stones[i];j--){//遍历背包
dp[j]=Math.max(dp[j],dp[j-stones[i]]+stones[i]);
}
}
return sum-dp[mid]-dp[mid];
}
}
494. 目标和
给你一个整数数组 nums
和一个整数 target
。
向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1]
,可以在2
之前添加'+'
,在1
之前添加'-'
,然后串联起来得到表达式"+2-1"
。
返回可以通过上述方法构造的、运算结果等于 target
的不同 表达式 的数目。
示例 1:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
1)题目解析
每个数字都有两种状态:被进行“+”, 或者被进行“-”,因此可以将数组分成A和B两个部分:sumA sumB
由推论可知:sum、target固定,所有问题就是在集合nums中找到和为sumA的组合
转换成01背包:装满容量为sumA的背包
2)思路解析
-
确定dp数组以及下标的含义
dp[j]
表示:填满j
(包括j
)这么⼤容积的包,有dp[i]
种⽅法 -
组合问题递推公式
dp[j] += dp[j - nums[i]]
-
数组初始化
从递归公式可以看出,在初始化的时候dp[0] ⼀定要初始化为1,因为dp[0]是在公式中⼀切递推结果的起源,如果dp[0]是0的话,递归结果将都是0。
dp[0] = 1,理论上也很好解释,装满容量为0的背包,有1种⽅法,就是装0件物品。
-
组合问题遍历顺序
01背包问题⼀维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。
3)代码实现
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum= 0;
for(int i:nums){
sum+=i;
}
if((sum+target)%2!=0) return 0;
if(target>sum) return 0;
int left = (sum+target)/2;
if(left<0) left = -left;
int[] dp = new int[left+1];
dp[0] = 1;
for(int i = 0;i<nums.length;i++){
for(int j=left;j>=nums[i];j--){
dp[j] += dp[j-nums[i]];
}
}
return dp[left];
}
}
474. 一和零
给你一个二进制字符串数组 strs
和两个整数 m
和 n
。
请你找出并返回 strs
的最大子集的长度,该子集中 最多 有 m
个 0
和 n
个 1
。
如果 x
的所有元素也是 y
的元素,集合 x
是集合 y
的 子集 。
示例 1:
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
1)题目解析
最多 有 m
个 0
和 n
个 1
:说明需要存储两个数, 0
和 1
的值,设置一个二维数组。strs 数组⾥的元素就是物品,每个物品都是⼀个!
⽽m 和 n相当于是⼀个背包,两个维度的背包。
2)思路解析
①最多 有 i
个 0
和 j
个 1
的strs
的最大子集的大小为dp[i][j]
②包含当前:dp[i][j] = dp[i-zero][j-one]+1
,其中zero,one
为当前strs[i]
的0和1
的个数
不包含当前:dp[i][j] =dp[i][j]
,从后往前遍历等于原来的
③dp[0][0]
即0个0 和 0个1
的最大子集的大小为0
④此题物品就是strs⾥的字符串,背包容量就是题⽬描述中的m和n。
for (string str : strs) { // 遍历物品
int oneNum = 0, zeroNum = 0;
for (char c : str) {
if (c == '0') zeroNum++;
else oneNum++;
}
for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历!
for (int j = n; j >= oneNum; j--) {
dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
}
}
}
3)代码实现
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp = new int[m+1][n+1];
for( String str : strs){
int zero =0;
int one=0;
char[] arr = str.toCharArray();
for(char c:arr){
if( c=='0') zero++;
else one++;
}
for(int i=m;i>=zero;i--){
for(int j=n;j>=one;j--){
dp[i][j] = Math.max(dp[i][j],dp[i-zero][j-one]+1);
}
}
}
return dp[m][n];
}
}
——完全背包问题
完全背包,即数组中的元素可重复使用,nums放在外循环,target在内循环。且内循环正序。
for num in nums:
for i in range(nums, target+1):
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
518. 零钱兑换 II
给你一个整数数组 coins
表示不同面额的硬币,另给一个整数 amount
表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0
。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例 1:
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
1)思路解析
①dp[i]
表示金额为i
的硬币有dp[i]
种方法
②dp[i] = Math.max(dp[i-coins[i]],dp[i-1])
①注意点:要确保i
比coins[i]
大
③dp[0]=1
:金额为0
有一种方法,不放。
先遍历物品在遍历背包:从小到大遍历
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包
dp[j] += dp[j - coins[i]];
}
}
2)代码实现
class Solution {
public int change(int amount, int[] coins) {
int n = coins.length;
int[] dp = new int[amount+1];
dp[0] = 1;
for(int i=0;i<n;i++){ //先遍历物品
for(int j=coins[i];j<=amount;j++){ //在遍历背包
dp[j]+=dp[j-coins[i]];
}
}
return dp[amount];
}
}
377. 组合总和 Ⅳ
给你一个由 不同 整数组成的数组 nums
,和一个目标整数 target
。请你从 nums
中找出并返回总和为 target
的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
示例 1:
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
1)思路解析
dp[i]
表示总和为i
的元素组合的个数
dp[i] += dp[i-nums[i]]
dp[0] = 1
; 确保j-nums[i]=0
的时候有值
**排列问题:背包放在外循环,将物品放在内循环
2)代码实现
class Solution {
public int combinationSum4(int[] nums, int target) {
int n = nums.length;
int[] dp = new int[target+1];
dp[0] = 1;
for(int j=0;j<=target;j++){
for(int i=0;i<n;i++){
if(j-nums[i]>=0)
dp[j]+=dp[j-nums[i]];
}
}
return dp[target];
}
}
139. 单词拆分
给你一个字符串 s
和一个字符串列表 wordDict
作为字典。请你判断是否可以利用字典中出现的单词拼接出 s
。
**注意:**不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
1)思路解析
dp[i]
表示s
从0
到i
是否可以由wordDict
拼接而成。
判断dp[i]
是否为true
2)代码实现
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int n = s.length();
boolean[] dp = new boolean[n+1];
dp[0]=true;
for(int i=1;i<=n;i++){
for(int j=0;j<i;j++){
if(dp[j] && wordDict.contains(s.substring(j,i))){
dp[i]=true;
break;
}
}
}
return dp[n];
}
}
——打家劫舍
337. 打家劫舍 III
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root
。
除了 root
之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root
。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
示例 1:
输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
1)思路解析
dp[i]
表示i
个房子能盗取的最大金额
先确定树的遍历顺序:根左右遍历 money[] left[] right[]
当前节点有两种状态:
①偷+左右节点不能偷 money[0] = root.val +left[1]+right[1]
②不偷+左右节点可以偷 [1]
money[1] = Math.max(left[1],left[0])+Math.max(right[1],right[0]);
通过递归一直获取左右节点
2)代码实现
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int rob(TreeNode root) {
int[] result = tree(root);
return Math.max(result[0],result[1]);
}
public int[] tree(TreeNode root){
if(root==null) return new int[]{0,0};
int[] left = tree(root.left);
int[] right = tree(root.right);
//偷cur
int money1 = root.val+left[0]+right[0];
//不偷
int money0 = Math.max(left[0],left[1])+ Math.max(right[0],right[1]);
return new int[]{money0,money1};
}
}
——买卖股票
121. 买卖股票的最佳时机
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
。
示例 1:
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
1)思路解析
思路还是挺清晰的,还是DP思想:
- 记录【今天之前买入的最小值】
- 计算【今天之前最小值买入,今天卖出的获利】,也即【今天卖出的最大获利】
- 比较【每天的最大获利】,取最大值即可
2)代码实现
class Solution {
public int maxProfit(int[] prices) {
if(prices.length <= 1)
return 0;
int min = prices[0], max = 0;
for(int i = 1; i < prices.length; i++) {
max = Math.max(max, prices[i] - min);
min = Math.min(min, prices[i]);
}
return max;
}
}
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 。
1)思路解析
每一天都有两种状态:①手上有股票 [0]
②手上没有股票 [1]
初始化:①第一天有股票 -price[0]
②第一天无股票 0
遍历顺序:一次遍历 有两种状态
手上有股票 :前一天的股票/之前没有股票今天买的股票
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-price[i])
手上没有股票 :前一天没有股票/今天卖了
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]+price[i])
2)代码实现
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
if(n==1) return 0;
int[][] dp = new int[n][2];
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i = 1 ;i < n;i++){
dp[i][0] = Math.max(dp[i-1][0],-prices[i]+dp[i-1][1]);
//手上有股票 前一天的股票/之前没有股票今天买的股票
dp[i][1] = Math.max(dp[i-1][1],prices[i]+dp[i-1][0]);
//手上没有股票 前一天没有股票/之前有股票今天卖了
}
return Math.max(dp[n-1][0],dp[n-1][1]);
}
}
188. 买卖股票的最佳时机 IV
给定一个整数数组 prices
,它的第 i
个元素 prices[i]
是一支给定的股票在第 i
天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
1)思路解析
每一天有2k+1
个状态:
dp[i][0]:
无股票 dp[0][0]=0
dp[i][1]:
第一天买入
dp[i][2]:
第一天卖出
dp[i][3]:
第二天买入
dp[i][4]:
第二天卖出
·······
dp[i][2k-1]:
第k天买入
dp[i][2k]:
第k天卖出
当前日没有操作,则延续之前的。
2)代码实现
class Solution {
public int maxProfit(int k, int[] prices) {
int n = prices.length;
if(n<=1 ){
return 0;
}
int[][] dp = new int[n][2*k+1];
dp[0][0] = 0;
for(int i=1;i<2*k+1;i++){
if(i%2!=0){
dp[0][i] = -prices[0];
}
}
for(int i=1;i<n;i++){
for(int j=0;j<2*k+1;j++){
if(j==0){
dp[i][j] = dp[i-1][j];
}else if(j%2==1){
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-1]-prices[i]);
}else{
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-1]+prices[i]);
}
}
}
return dp[n-1][2*k];
}
}
——子序列问题
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 。
1)思路解析
dp[i]:
表示从0
到i
的最长递增子序列
if(nums[i] > nums[j]) dp[i] = Math.max(dp[j]+1,dp[i]);
注意这⾥不是要dp[i]与 dp[j] + 1进⾏⽐较,⽽是我们要取dp[j] + 1]的最⼤值:即求j
之前的最大递增子序列的大小
每一个单独数字都是一个递增子序列:Arrays.fill(dp,1);
2)代码解析
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
if(n==1){
return 1;
}
int[] dp = new int[n];
//从0到i的最长子序列
int max=0;
Arrays.fill(dp,1);
//每一个单独数字都是一个递增子序列
for(int i = 1;i<n;i++){
for(int j=0;j<i;j++){
//从i到j
if(nums[i]>nums[j]){
dp[i]=Math.max(dp[i],dp[j]+1);
}
}
max=Math.max(dp[i],max);
}
return max;
}
}
674. 最长连续递增序列
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。
连续递增的子序列 可以由两个下标 l
和 r
(l < r
)确定,如果对于每个 l <= i < r
,都有 nums[i] < nums[i + 1]
,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]]
就是连续递增子序列。
示例 1:
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
1)思路解析
//贪心算法
①只要递增 数值加一 取最大值
②不递增将数值变为1
2)代码解析
class Solution {
public int findLengthOfLCIS(int[] nums) {
int n = nums.length;
if(n == 1){
return 1;
}
int max = 1;
int count=1;
for(int i=0;i<n-1;i++){
if(nums[i+1]>nums[i]){
count++;
}else{
count=1;
}
max=Math.max(count,max);
}
return max;
}
}
718. 最长重复子数组
给两个整数数组 nums1
和 nums2
,返回 两个数组中 公共的 、长度最长的子数组的长度 。
示例 1:
输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。
1)思路解析
子数组就是子序列
求连续子序列dp[i][j]
由dp[i-1][j-1]
得出
当A[i-1]==B[j-1] dp[i][j]=dp[i-1][j-1]+1
在遍历的时候将dp
最大值记录
2)代码解析
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int n1 = nums1.length;
int n2 = nums2.length;
int[][] dp= new int[n1+1][n2+1];
int max =0;
for(int i=1;i<=n1;i++){
for(int j=1;j<=n2;j++){
if(nums1[i-1] == nums2[j-1]){
dp[i][j]=dp[i-1][j-1]+1;
}
max=Math.max(dp[i][j],max);
}
}
return max;
}
}
1143. 最长公共子序列
给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,
"ace"
是"abcde"
的子序列,但"aec"
不是"abcde"
的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
1)思路解析
主要就是两⼤情况:
text1[i - 1
] 与 text2[j - 1]
相同,text1[i - 1]
与 text2[j - 1]
不相同
①如果text1[i - 1]
与 text2[j - 1]
相同,那么找到了⼀个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;
②如果text1[i - 1]
与 text2[j - 1]
不相同,那就看看text1[0, i - 2]
与text2[0, j - 1]
的最⻓公共⼦序列 和text1[0, i - 1]
与text2[0, j - 2]
的最⻓公共⼦序列,取最⼤的。
即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 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]);
}
1143. 最长公共子序列
2)代码解析
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int n1= text1.length();
int n2=text2.length();
int max=0;
char[] text1Arr = text1.toCharArray();
char[] texr2Arr = text2.toCharArray();
int[][] dp = new int[n1+1][n2+1];
for(int i=1;i<=n1;i++){
for(int j=1;j<=n2;j++){
if(text1Arr[i-1]==texr2Arr[j-1]){
dp[i][j] = dp[i-1][j-1]+1;
}else{
dp[i][j] = Math.max(dp[i][j-1],dp[i-1][j]);
}
max = Math.max(dp[i][j],max);
}
}
return max;
}
}
有的子序列。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
1)思路解析
主要就是两⼤情况:
text1[i - 1
] 与 text2[j - 1]
相同,text1[i - 1]
与 text2[j - 1]
不相同
①如果text1[i - 1]
与 text2[j - 1]
相同,那么找到了⼀个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;
②如果text1[i - 1]
与 text2[j - 1]
不相同,那就看看text1[0, i - 2]
与text2[0, j - 1]
的最⻓公共⼦序列 和text1[0, i - 1]
与text2[0, j - 2]
的最⻓公共⼦序列,取最⼤的。
即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 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]);
}
2)代码解析
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int n1= text1.length();
int n2=text2.length();
int max=0;
char[] text1Arr = text1.toCharArray();
char[] texr2Arr = text2.toCharArray();
int[][] dp = new int[n1+1][n2+1];
for(int i=1;i<=n1;i++){
for(int j=1;j<=n2;j++){
if(text1Arr[i-1]==texr2Arr[j-1]){
dp[i][j] = dp[i-1][j-1]+1;
}else{
dp[i][j] = Math.max(dp[i][j-1],dp[i-1][j]);
}
max = Math.max(dp[i][j],max);
}
}
return max;
}
}