java动态规划


动态规划

一、适合问题

存在若干步骤,并且每个步骤都面临若干选择。如果要求列出所有解就是回溯法。如果是求最优解(通常是最大值最小值)就用动态规划。

1.波契那亚数列和爬楼梯
2.背包问题
3.打家劫舍
4.股票问题
5.子序列问题

二、动态规划五部曲

1.dp数组及实际含义

数组dp用来保存每个问题结果的缓存,避免重复计算。

2.dp数组如何初始化

3.递推公式

用一个等式表示其中某一步的最优解和前面若干步的最优解的关系。

4.遍历顺序

for(i   背包)
	for(j   物品)

5.打印dp数组

三、斐波那契数

斐波那契数,通常⽤ F(n) 表⽰,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后⾯的每⼀项数字都是前⾯两项数字的和。也就是:1 1 2 3 5 8
F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1
给你n ,请计算 F(n) 。

1.确定dp[i]含义 dp[i]:第i个斐波那契数值为dp[i]。

2.递推公式 dp[i]=dp[i-1]+dp[i-2]

3.dp数组如何初始化 dp[0]=1 dp[1]=1

4.遍历顺序 从前到后(根据递推公式来的)

5.打印dp数组

四、爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的⽅法可以爬到楼顶呢?
注意:给定 n 是⼀个正整数。
⽰例 1:
输⼊: 2
输出: 2
解释: 有两种⽅法可以爬到楼顶。
1 阶 + 1 阶
2 阶
(依赖于前两阶)

几阶
1阶 1种
2阶 2种
3阶 3种
4阶 5种

1.确定dp[i]含义 dp[i]:到达i阶有dp[i]种方法

2.递推公式 dp[i]=dp[i-1]+dp[i-2](第i阶是由i-1阶和i-2阶得来的,因为只要再走一步or二步)

3.dp数组如何初始化 dp[0]=1 dp[1]=1 dp[2]=2

4.遍历顺序:从前到后

5.打印dp数组

五、使⽤最⼩花费爬楼梯

数组的每个下标作为⼀个阶梯,第 i 个阶梯对应着⼀个⾮负数的体⼒花费值 cost[i](下标从0 开始)。每当你爬上⼀个阶梯你都要花费对应的体⼒值,⼀旦⽀付了相应的体⼒值,你就可以选择向上爬⼀个阶梯或者爬两个阶梯。请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
⽰例 1:
输⼊:cost = [10, 15, 20]
输出:15
解释:最低花费是从 cost[1] 开始,然后⾛两步即可到阶梯顶,⼀共花费 15 。

解释:最低花费⽅式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,⼀共花费 6 。

1.确定dp[i]含义 dp[i]:到达第i个台阶的最小花费为dp[i]

2.递推公式dp[i]=min((dp[i-1]+cost[i-1]),(dp[i-2]+cost[i-2]))

dp[i]
dp[i-1]+cost[i-1]
dp[i-2]+cost[i-2]

3.dp数组如何初始化 dp[0]=0 dp[1]=0(一开始可以选择哪个台阶开始,还不用跳,都可以为0

4.遍历顺序:从前到后

5.打印dp数组

六、不同路径

⼀个机器⼈位于⼀个 m x n ⽹格的左上⾓ (起始点在下图中标记为 “Start” )。
机器⼈每次只能向下或者向右移动⼀步。机器⼈试图达到⽹格的右下⾓(在下图中标记为
“Finish” )。
问总共有多少条不同的路径?
⽰例 1:
java动态规划_第1张图片

1.确定dp[i][j]含义 dp[i][j]:到达位置(i,j)的路径数

2.递推公式dp[i][j]=dp[i][j-1]+dp[i-1][j]

3.dp数组如何初始化 (第一行跟第一列必须初始化,如果这两个没有初始化没办法接下来计算)

dp[0][j]=1
dp[i][0]=1
for (int i=0;i for(int j=0;j

4.遍历顺序:从上到下,从左到右遍历

5.打印dp数组

六、不同路径II

⼀个机器⼈位于⼀个 m x n ⽹格的左上⾓ (起始点在下图中标记为“Start” )。
机器⼈每次只能向下或者向右移动⼀步。机器⼈试图达到⽹格的右下⾓(在下图中标记为
“Finish”)。
现在考虑⽹格中有障碍物。那么从左上⾓到右下⾓将会有多少条不同的路径?
java动态规划_第2张图片
输⼊:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:
3x3 ⽹格的正中间有⼀个障碍物。
从左上⾓到右下⾓⼀共有 2 条不同的路径:

  1. 向右 -> 向右 -> 向下 -> 向下
  2. 向下 -> 向下 -> 向右 -> 向右

1.确定dp[i]含义 dp[i]:到达i阶有dp[i]种方法

2.递推公式dp[i][j]=dp[i][j-1]+dp[i-1][j](多一个条件就是遇到障碍不用继续推导)

if(obs[i][j]==0) 当遇不上障碍时开始推导。

3.dp数组如何初始化 (第一行跟第一列必须初始化,如果这两个没有初始化没办法接下来计算)但是如果这两行遇到障碍,之后都是0

for(int i=0;i for(int j=0;j

4.遍历顺序:从上到下,从左到右遍历

5.打印dp数组

七、整数拆分

给定⼀个正整数 n,将其拆分为⾄少两个正整数的和,并使这些整数的乘积最⼤化。 返回
你可以获得的最⼤乘积。
⽰例 1:
输⼊: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

1.确定dp[i]含义 dp[i]:是给定一个数i,能拆得最大乘积

j×(i-j) 拆成2个
j×dp[i-j]拆成3个或3个以上

2.递推公式dp[i]=max(j*(i-j),j*dp[i-j])

3.dp数组如何初始化

dp[0]=0(没有意义)
dp[1]=0(没有意义)
dp[2]=1

3.dp数组如何初始化

从dp[3]开始,因为dp[0],dp[1],dp[2]都已经初始化了。
for(i=3;i<=n;i++)
for(j=1;j dp[i]=max(j*(i-j),j*dp[i-j],dp[i])

4.遍历顺序

4.打印dp数组

八、不同的二叉搜索树

java动态规划_第3张图片

1.确定dp[i]含义 dp[i]:1到i为节点组成的⼆叉搜索树的个数为dp[i]。

2.递推公式dp[i]=max(j*(i-j),j*dp[i-j])

在上⾯的分析中,其实已经看出其递推关系, dp[i] += dp[以j为头结点左⼦树节点数量] *
dp[以j为头结点右⼦树节点数量]
j相当于是头结点的元素,从1遍历到i为⽌。
所以递推公式:dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左⼦树节点数量,i-j 为以j为
头结点右⼦树节点数量

3.dp数组如何初始化

dp[0]=1(空树)

4.遍历顺序:从小到大

for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
dp[i]+=dp[j-1]*dp[i-j];

5.打印dp数组

九、背包问题

java动态规划_第4张图片

1.确定dp[i][j]含义 dp[i][j]:即dp[i][j] 表⽰从下标为[0-i]的物品⾥任意取,放进容量为j的背包,价值总和最⼤是多少

2.递推公式

对于每个物品有两种选择:放与不放
不放物品i:dp[i-1][j]
放物品i:dp[i-1][j-weight]+value
dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight]+value)

3.dp数组如何初始化

递推公式是由上一行推导来的,所以要初始化第一行和第一列。第一列都为0,因为代表0重量情况下无法装入任何物品。第一行如果编号i物品可以装入,则是该物品的价值,如果重量导致不能装入则为0

4.遍历顺序:可以先遍历物品再遍历背包,也可以先遍历背包再遍历物品

for()物品
for()背包
二维数组无所谓谁先谁后,但是一维滚动数组就有顺序

十、背包问题-滚动数组

1.确定dp[j]含义 :在⼀维dp数组中,dp[j]表⽰:容量为j的背包,所背的物品价值可以最⼤为dp[j]。

2.递推公式

dp[j]=max(dp[j],dp[j-weight[i]]+value[i])
可以看出相对于⼆维dp数组的写法,就是把dp[i][j]中i的维度去掉了。

3.dp数组如何初始化

关于初始化,⼀定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
dp[j]表⽰:容量为j的背包,所背的物品价值可以最⼤为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最⼤价值就是0。
那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢?
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);看⼀下递归公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
dp数组在推导的时候⼀定是取价值最⼤的数,如果题⽬给的价值都是正整数那么⾮0下标都
初始化为0就可以了,如果题⽬给的价值有负数,那么⾮0下标就要初始化为负⽆穷。
这样才能让dp数组在递归公式的过程中取的最⼤的价值,⽽不是被初始值覆盖了。
那么我假设物品价值都是⼤于0的,所以dp数组初始化的时候,都初始为0就可以了。

4.遍历顺序:一维dp遍历,背包从大到小,倒序遍历为了保证物品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]);
}
}

5.打印dp数组

十一、分割等和子集

给定⼀个只包含正整数的⾮空数组。是否可以将这个数组分割成两个⼦集,使得两个⼦集的
元素和相等。
注意:
每个数组中的元素不会超过 100
数组的⼤⼩不会超过 200
⽰例 1:
输⼊: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].

抽象成背包问题,容量和价值一样,比如把容量11的背包装满之后价值也是11.

1.确定dp[j]含义 :在⼀维dp数组中,dp[j]表⽰:容量为j的背包,所背的物品价值可以最⼤为dp[j]。

2.递推公式

dp[i][j]=max(dp[i-1][j],dp[i-1][j-weightt[i]+value[i])
降维了
dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);

3.dp数组如何初始化

dp[0]=0
整个dp数组初始化为0

4.遍历顺序:先遍历物品后遍历背包,背包是倒叙防止重复

// 开始 01背包
for(int i = 0; i < nums.size(); i++) {
	for(int j = target; j >= nums[i]; j--) { // 每⼀个元素⼀定是不可重复放
⼊,所以从⼤到⼩遍历
		dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}

5.打印dp数组

十二、最后⼀块⽯头的重量II

有⼀堆⽯头,每块⽯头的重量都是正整数。
每⼀回合,从中选出任意两块⽯头,然后将它们⼀起粉碎。假设⽯头的重量分别为 x 和 y,
且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块⽯头都会被完全粉碎;
如果 x != y,那么重量为 x 的⽯头将会完全粉碎,⽽重量为 y 的⽯头新重量为 y-x。
最后,最多只会剩下⼀块⽯头。返回此⽯头最⼩的可能重量。如果没有⽯头剩下,就返回
0。
⽰例:
输⼊:[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.确定dp[j]含义 :装满容量为j的最大重量是dp[j]

2.递推公式

dp[j]=max[dp[j],dp[j-weight[i]+value[i]]]

3.dp数组如何初始化

dp[0]=0
整个dp数组初始化为0
定义多大的数组,sum/2
最后结果是sum-dp[target]-dp[target]

4.遍历顺序:先遍历物品后遍历背包,背包是倒叙防止重复

5.打印dp数组

十三、目标和

给定⼀个⾮负整数数组,a1, a2, …, an, 和⼀个⽬标数,S。现在你有两个符号 + 和 -。对于
数组中的任意⼀个整数,你都可以从 + 或 -中选择⼀个符号添加在前⾯。
返回可以使最终数组和为⽬标数 S 的所有添加符号的⽅法数。
⽰例:
输⼊:nums: [1, 1, 1, 1, 1], S: 3
输出:5
解释:
-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
⼀共有5种⽅法让最终⽬标和为3。

left+right=Sum
left-right=target
right=Sum-left
left-(Sum-left)=target
left=(target+Sum)/2

1.确定dp[j]含义 :装满容量为j,有dp[j]种方法

2.递推公式

dp[j]+=dp[j-num[i]]

3.dp数组如何初始化

从递归公式可以看出,在初始化的时候dp[0] ⼀定要初始化为1,因为dp[0]是在公式中⼀切
递推结果的起源,如果dp[0]是0的话,递归结果将都是0。
dp[0] = 1,理论上也很好解释,装满容量为0的背包,有1种⽅法,就是装0件物品。
dp[j]其他下标对应的数值应该初始化为0,从递归公式也可以看出,dp[j]要保证是0的初始
值,才能正确的由dp[j - nums[i]]推导出来。

4.遍历顺序:先遍历物品后遍历背包,背包是倒叙防止重复

for(i=0;i for(j=bagsize;j>=nums[i];j–)

十四、一和零

给你一个二进制字符串数组 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 。

示例 2:

输入:strs = [“10”, “0”, “1”], m = 1, n = 1
输出:2
解释:最大的子集是 {“0”, “1”} ,所以答案是 2 。
提示:

1 <= strs.length <= 600
1 <= strs[i].length <= 100
strs[i] 仅由 ‘0’ 和 ‘1’ 组成
1 <= m, n <= 100

1.确定dp[i][j]含义 :最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。

思路:一个容器有两个维度。m和n。

2.递推公式

dp[i][j]=max(dp[i-x][j-y]+1,dp[i][j])

3.dp数组如何初始化

dp[0][0]=0
非0也初始化为0

4.遍历顺序:先遍历物品后遍历背包,背包是倒叙防止重复

class Solution{
	public int findMaxForm(String[] strs,int m,int n){
		int[][] dp=new [m+1][n+1];
		int oneNum,zeroNum;
		for (String str:strs){
			oneNum=0;
			zeroNum=0;
			for(char ch:str.toCharArray()){
				if(ch=='0'){
					zeroNum++;
				}else{
					oneNum++;
				}
			}
			//倒序遍历
			for(int i=m;i>=zeroNum;i--){
				for(int j=n;j>=oneNum;j--){
					dp[i][j]=Math.max(dp[i][j],dp[i-zeroNum][j-oneNum]+1);
				}
			}
		}
		return dp[m][n];
	}
}

十五、完全背包理论

java动态规划_第5张图片

完全背包和01背包唯一不同的地方,每种物品有无限件。在代码中体现在遍历顺序上。
01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了,一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量。

在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。遍历物品在外层循环,遍历背包容量在内层循环。

private static void testCompletePack(){
	int[] weight={1,3,4};
	int[] value={15,20,30};
	int bagWeight=4;
	int[] dp=new int[bagWeight+1];
	for (int i=0;i<weight.length;i++){
		for(int j=weight[i];j<=bagWeight;j++){
			dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]);
		}
	}
	for(int maxValue:dp){
		System.out.println(maxValue+" ");
	}
}

十六、零钱兑换2

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

示例 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[j]含义 :以总金额j的最多硬币组合数

2.递推公式

dp[j]+=dp[j-conins[i]]
求装满背包有几种方法,公式都是:dp[j] += dp[j - nums[i]];

2.初始化dp数组

首先dp[0]一定要为1,dp[0] = 1是 递归公式的基础。如果dp[0] = 0 的话,后面所有推导出来的值都是0了。

3.遍历顺序

求组合就先遍历物品,求排序就先遍历背包

class Solution{
	public int change(int amount,int[] coins){
		int[] dp=new int[amount+1];
		dp[0]=1;
		for(int i=0;i<coins.length;i++){
			for(int j=coins[i];j<=amount;j++){
				dp[j]+=dp[j-coins[i]];
			}
		}
		return dp[amount];
	}
}

十七、组合总和

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

示例:

nums = [1, 2, 3]
target = 4
所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1)

请注意,顺序不同的序列被视作不同的组合。

因此输出为 7。

1.确定dp[i]含义 :凑成目标i的组合数总和

2.递推公式

dp[i]+=dp[i-nums[j]]

3.dp数组如何初始化

因为递推公式dp[i] += dp[i - nums[j]]的缘故,dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础。
至于非0下标的dp[i]应该初始为多少呢?
初始化为0,这样才不会影响dp[i]累加所有的dp[i - nums[j]]。

4.遍历顺序:因为求的是排列,所以先背包后物品

如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。

class Solution{
	int[] dp=new int[target+1];
	do[0]=1;
	for(int i=0;i<=target;i++){
		for(int j=0;j<=nums.length;j++){
			if(i>=nums[j]){
				dp[i]+=dp[i-nums[j]];
			}
		}
	}
	return dp[target];
	
}

十八、爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1: 输入: 2 输出: 2 解释: 有两种方法可以爬到楼顶。

1 阶 + 1 阶
2 阶
示例 2: 输入: 3 输出: 3 解释: 有三种方法可以爬到楼顶。

1 阶 + 1 阶 + 1 阶
1 阶 + 2 阶
2 阶 + 1 阶

1.确定dp[i]含义 :爬到有i个台阶的楼顶,有dp[i]种方法。

2.递推公式

dp[i] += dp[i - j]

3.数组初始化

既然递归公式是 dp[i] += dp[i - j],那么dp[0] 一定为1,dp[0]是递归中一切数值的基础所在,如果dp[0]是0的话,其他数值都是0了。

下标非0的dp[i]初始化为0,因为dp[i]是靠dp[i-j]累计上来的,dp[i]本身为0这样才不会影响结果

4.遍历顺序

这是背包里求排列问题,即:1、2 步 和 2、1 步都是上三个台阶,但是这两种方法不一样!

所以需将target放在外循环,将nums放在内循环。

每一步可以走多次,这是完全背包,内循环需要从前向后遍历。

class Solution {
    public int climbStairs(int n) {
        int[] dp = new int[n + 1];
        int m = 2;
        dp[0] = 1;

        for (int i = 1; i <= n; i++) { // 遍历背包
            for (int j = 1; j <= m; j++) { //遍历物品
                if (i >= j) dp[i] += dp[i - j];
            }
        }

        return dp[n];
    }
}

十九、零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1

1.确定dp[j]含义 :凑足总额为j所需钱币的最少个数为dp[j]

2.递推公式

dp[j]=min([dp[ j-coins[i]+1,dp[j]])

3.初始化数组

dp[0]=0
但是本题是取最小值,所以非0下标初始化为最大值

4.遍历顺序

都可以

 class Solution{
 	public int coinChange(int[] coins,int amount){
 		int max=Integer.MAX_VALUE;
 		int[] dp=new int[amount+1];
 		for(int j=0;j<dp.length;j++){
 			dp[j]=max;
 		}
 		dp[0]=0;
 		for(int i=0;i<coins.length;i++){
 			for(int j=coins[i];j<=amount;j++){
 				if(dp[j-coins[i]]!=max){
 					dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);
 				}
 			}
 		}
 		return dp[amount]==max?-1:dp[amount];
 	}
 }

二十、完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

示例 1:

输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:

输入:n = 13
输出:2
解释:13 = 4 + 9
提示:

1 <= n <= 10^4

我来把题目翻译一下:完全平方数就是物品(可以无限件使用),凑个正整数n就是背包,问凑满这个背包最少有多少物品?

1.确定dp[j]含义 :和为j的完全平方数的最少数量为dp[j]

2.确定递推函数

dp[j]=min(dp[j-i*i]+1,dp[j])

3.dp数组如何初始化

dp[0]表示 和为0的完全平方数的最小数量,那么dp[0]一定是0。非0的是整数最大值,因为递推公式是求最小值。

4.遍历顺序

都可以

class Solution{
	public int numSquares(int n){
		int max=Integer.MAX_VALUE;
		int[] dp=new int[n+1];
		for(int j=0;j<=n;j++){
			dp[j]=max;
		}
		for(int i=1;i*i<=n;i++){
			for(int j=i*i;j<=n;j++){
				if(dp[j-i*i]!=n){
					dp[j]=Math.min(dp[j],dp[j-i*i]+1);
				}
			}
		}
		return dp[n];
	}
}

二十一、单词拆分

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:

拆分时可以重复使用字典中的单词。

你可以假设字典中没有重复的单词。

示例 1:

输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。

1.dp数组含义

dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。

2.递推公式

如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。

所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。

3.初始化

dp[0]=true,不然后面递推都是false

4.有顺序要求的

先遍历背包,后遍历物品

class Solution {
	public boolean wordBreak(String s,List<String> wordDict){
		boolean[] dp=new boolean[s.length+1];
		dp[0]=true;
		for(int i=1;i<=s.length();i++){
			for(String word:wordDict){
				int len=word.length();
				if(i>=len&&dp[i-len]&&word.equals(s.substring(i-len,i))){
					dp[i]=true;
					break;
				}
			}
		}
		return dp[s.length()];
	}
}

二十二、打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。

1.确定dp[i]含义 :考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]

2.确定递归公式

决定dp[i]的因素是第i间房间偷还是不偷。
如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i] ,即:第i-1房一定是不考虑的,找出 下标i-2(包括i-2)以内的房屋,最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱。

如果不偷第i房间,那么dp[i] = dp[i - 1],即考 虑i-1房,(注意这里是考虑,并不是一定要偷i-1房,这是很多同学容易混淆的点)

然后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);

3.数组初始化

从递推公式dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);可以看出,递推公式的基础就是dp[0] 和 dp[1]

从dp[i]的定义上来讲,dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]);

4.遍历顺序

按照递归的顺序,肯定是从前到后遍历

//进一步对滚动数组的空间优化 dp数组只存与计算相关的两次数据
class Solution{
	public int rob(int[] nums){
		if(nums.length==1){
			return nums[0];
		}
		//初始化dp数组
		//优化空间dp数组只用2格空间 只记录与当前计算相关的前两个结果
		int[] dp=new int[2];
		dp[0]=nums[0];
		dp[1]=Math.max(nums[0],nums[1]);
		int res=0;
		for(int i=2;i<nums.length;i++){
			res=Math.max(dp[0]+nums[i],dp[1]);
			dp[0]=dp[1];
			dp[1]=res;
	}
		return dp[1];
	}
}

二十三、打家劫舍2

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。

示例 1:

输入:nums = [2,3,2]

输出:3

解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入:nums = [1,2,3,1]

输出:4

解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。

示例 3:

输入:nums = [0]

输出:0

class Solution{
	public int rob(int[] nums){
		if(nums==null ||nums.length==0){
			return 0;
		}
		int len=nums.length;
		if(len==1){
			return nums[0];
		}
		return Math.max(robAction(nums,0,len-1),robAction(nums,1,len))
	}
	int  robAction(int[] nums,int start,int end){
		int x=0,y=0,z=0;
		for(int i=start;i<end;i++){
			y=z;
			z=Math.max(y,x+nums[i]);
			x=y;
		}
		return z;
	}
}

二十四、打家劫舍3

java动态规划_第6张图片
动态规划其实就是使用状态转移器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的最大金钱。

1.确定递归函数的参数和返回值

这里我们要求一个节点偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。
dp数组的含义:下标为0记录不偷该节点所得到的最大金钱,下标为1记录偷该节点所得到的最大金钱。

2.确定终止条件

在遍历过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以返回。相当于dp数组的初始化。

3.确定遍历顺序

首先明确的是使用后续遍历。因为要通过递归函数的返回值来做下一步的计算。
通过递归左节点,得到左节点偷与不偷的金钱。
通过递归右节点,得到右节点偷与不偷的金钱。

4.确定单层递归的逻辑

如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0]; (如果对下标含义不理解就再回顾一下dp数组的含义)

如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]);

最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱}

class Solution{
	    // 3.状态标记递归
    // 执行用时:0 ms , 在所有 Java 提交中击败了 100% 的用户
    // 不偷:Max(左孩子不偷,左孩子偷) + Max(又孩子不偷,右孩子偷)
    // root[0] = Math.max(rob(root.left)[0], rob(root.left)[1]) +
    // Math.max(rob(root.right)[0], rob(root.right)[1])
    // 偷:左孩子不偷+ 右孩子不偷 + 当前节点偷
    // root[1] = rob(root.left)[0] + rob(root.right)[0] + root.val;
    public int rob3(TreeNode root){
    	int[] res=robAction1(root);
    	return Math.max(res[0],res[1]);
    }
    int[] robAction1(TreeNode root){
    	int res[]=new int[2];
    	if(root==null)
    		return res;
    	int[] left=robAction1(root.left);
    	int[] right=robAction1(root.right);

		res[0]=Math.max(left[0],left[1])+Math.min(right[0],right[1]);
		res[1]=root.val+left[0]+right[0];
		return res;
    }
}

二十五、买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

示例 1:

输入:[7,1,5,3,6,4]

输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:

输入:prices = [7,6,4,3,1]

输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

1.确定dp数组的含义:

dp[i][0]表示第i天持有股票所得最多现金。
其实一开始现金是0,那么加入第i天买入股票现金就是 -prices[i], 这是一个负数。

dp[i][1]表示第i天不持有股票所得最多现金。
注意这里说的是“持有”,“持有”不代表就是当天“买入”!也有可能是昨天就买入了,今天保持持有的状态。

2.确定递推公式

如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来

第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
第i天买入股票,所得现金就是买入今天的股票后所得现金即:-prices[i]
那么dp[i][0]应该选所得现金最大的,所以dp[i][0] = max(dp[i - 1][0], -prices[i]);

如果第i天不持有股票即dp[i][1], 也可以由两个状态推出来

第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金即:prices[i] + dp[i - 1][0]
同样dp[i][1]取最大的,dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);

3.dp数组初始化

那么dp[0][0]表示第0天持有股票,此时的持有股票就一定是买入股票了,因为不可能有前一天推出来,所以dp[0][0] -= prices[0];

dp[0][1]表示第0天不持有股票,不持有股票那么现金就是0,所以dp[0][1] = 0;

4.遍历顺序

从前往后遍历

class Solution{
	public int maxProfit(int[] prices){
		if(prices==null || prices.length==0) return 0;
		int length=prices.length;
		//dp[i][0]代表第i天持有股票的最大收益
		//dp[i][1]代表第i天不持有股票的最大收益
		int[][] dp=new int[length][2];
		int result=0;
		dp[0][0]=-prices[0];
		dp[0][1]=0;
		for(int i=1;i<length;i++){
			dp[i][0]=Math.max(dp[i-1][0],-prices[i]);
			dp[i][1]=Math.max(dp[i-1][1]+prices[i],dp[i-1][1]);
		}
		return dp[length-1][1];
	}
}

二十六、买卖股票的最佳时机2

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

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

示例 1:

输入: [7,1,5,3,6,4]

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

示例 2:

输入: [1,2,3,4,5]

输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

输入: [7,6,4,3,1]

输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

提示:

1 <= prices.length <= 3 * 10 ^ 4
0 <= prices[i] <= 10 ^ 4

1.确定dp数组的含义:

dp[i][0]表示第i天持有股票所得现金
dp[i][1]表示第i天不持有股票所得现金

如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来

第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i]

注意这里和121. 买卖股票的最佳时机 (opens new window)唯一不同的地方,就是推导dp[i][0]的时候,第i天买入股票的情况。

在121. 买卖股票的最佳时机 (opens new window)中,因为股票全程只能买卖一次,所以如果买入股票,那么第i天持有股票即dp[i][0]一定就是 -prices[i]。

而本题,因为一只股票可以买卖多次,所以当第i天买入股票的时候,所持有的现金可能有之前买卖过的利润。

那么第i天持有股票即dp[i][0],如果是第i天买入股票,所得现金就是昨天不持有股票的所得现金 减去 今天的股票价格 即:dp[i - 1][1] - prices[i]。

再来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来

第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金即:prices[i] + dp[i - 1][0]

class Solution{
	public int maxProfit(int[] prices){
		int[] dp=new int[2];
		//0表示持有,1表示卖出
		dp[0]=-prices[0];
		dp[1]=0;
		for(int i=1;i<=prices.length;i++){
			//前一天持有;既然不限制交易次数,那么再买股票时,要加上之前的收益
			dp[0]=Math.max(dp[0],dp[1]-prices[i-1]);
			//前一天卖出;或当天卖出,当天卖出,得先持有
			dp[1]=Math.max(dp[1],dp[0]+prices[i-1]);
		}
		return dp[1];
	}
}

二十七、买卖股票最佳时机3

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

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

示例 1:

输入:prices = [3,3,5,0,0,3,1,4]

输出:6 解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3。

示例 2:

输入:prices = [1,2,3,4,5]

输出:4 解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4。注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

输入:prices = [7,6,4,3,1]

输出:0 解释:在这个情况下, 没有交易完成, 所以最大利润为0。

示例 4:

输入:prices = [1] 输出:0

提示:

1 <= prices.length <= 10^5
0 <= prices[i] <= 10^5

1.确定dp数组的含义

一天一共就有五个状态,

没有操作 (其实我们也可以不设置这个状态)
第一次持有股票
第一次不持有股票
第二次持有股票
第二次不持有股票
dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金。

需要注意:dp[i][1],表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,这是很多同学容易陷入的误区。

例如 dp[i][1] ,并不是说 第i天一定买入股票,有可能 第 i-1天 就买入了,那么 dp[i][1] 延续买入股票的这个状态
java动态规划_第7张图片

2.递推公式

达到dp[i][1]状态,有两个具体操作:

操作一:第i天买入股票了,那么dp[i][1] = dp[i-1][0] - prices[i]
操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1]
那么dp[i][1]究竟选 dp[i-1][0] - prices[i],还是dp[i - 1][1]呢?

一定是选最大的,所以 dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);

同理dp[i][2]也有两个操作:

操作一:第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i]
操作二:第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2]
所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])

同理可推出剩下状态部分:

dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);

dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);

3.dp数组初始化

第0天没有操作,这个最容易想到,就是0,即:dp[0][0] = 0;

第0天做第一次买入的操作,dp[0][1] = -prices[0];

第0天做第一次卖出的操作,这个初始值应该是多少呢?

此时还没有买入,怎么就卖出呢? 其实大家可以理解当天买入,当天卖出,所以dp[0][2] = 0;

第0天第二次买入操作,初始值应该是多少呢?应该不少同学疑惑,第一次还没买入呢,怎么初始化第二次买入呢?

第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后再买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。

所以第二次买入操作,初始化为:dp[0][3] = -prices[0];

同理第二次卖出初始化dp[0][4] = 0;

4.确定遍历顺序 从前到后

//空间优化
class Solution{
	public int maxProfit(int[] prices){
		int[] dp=new int[4];
		//存储两次交易的状态就行了
		//dp[0]代表第一次交易的买入
		dp[0]=-prices[0];
		//dp[1]代表第一次交易的卖出
		dp[1]=0;
		//dp[2]代表第二次交易的买入
		dp[2]=-prices[0];
		//dp[3]代表第二次交易的卖出
		dp[3]=0;
		for(int i=1;i<=prices.length;i++){
			//要么保持不变,要么没有就买,有了就卖
			dp[0]=Math.max(dp[0],-prices[i-1]);
			dp[1]=Math.max(dp[1],dp[0]+prices[i-1]);
			//这已经是第二次交易了,所以得加上前一次交易出去的收获
			dp[2]=Math.max(dp[2],dp[1]-prices[i-1]);
			dp[3]=Math.max(dp[3],dp[2]+prices[i-1]);
		}
		return dp[3];
	}
}

二十八、买卖股票的最佳时机4

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。

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

示例 1:

输入:k = 2, prices = [2,4,1]

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

示例 2:

输入:k = 2, prices = [3,2,6,5,0,3]

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

提示:

0 <= k <= 100
0 <= prices.length <= 1000
0 <= prices[i] <= 1000

1.确定dp数组含义

在动态规划:123.买卖股票的最佳时机III (opens new window)中,我是定义了一个二维dp数组,本题其实依然可以用一个二维dp数组。

使用二维数组 dp[i][j] :第i天的状态为j,所剩下的最大现金是dp[i][j]

j的状态表示为:

0 表示不操作
1 第一次买入
2 第一次卖出
3 第二次买入
4 第二次卖出

大家应该发现规律了吧 ,除了0以外,偶数就是卖出,奇数就是买入。

题目要求是至多有K笔交易,那么j的范围就定义为 2 * k + 1 就可以了。

2.确定递推公式

还要强调一下:dp[i][1],表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,这是很多同学容易陷入的误区。

达到dp[i][1]状态,有两个具体操作:

操作一:第i天买入股票了,那么dp[i][1] = dp[i - 1][0] - prices[i]
操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1]
选最大的,所以 dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1]);

同理dp[i][2]也有两个操作:

操作一:第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i]
操作二:第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2]
所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])
本题和动态规划:123.买卖股票的最佳时机III (opens new window)最大的区别就是这里要类比j为奇数是买,偶数是卖的状态。

3.dp数组初始化

第0天没有操作,这个最容易想到,就是0,即:dp[0][0] = 0;

第0天做第一次买入的操作,dp[0][1] = -prices[0];

第0天做第一次卖出的操作,这个初始值应该是多少呢?

此时还没有买入,怎么就卖出呢? 其实大家可以理解当天买入,当天卖出,所以dp[0][2] = 0;

第0天第二次买入操作,初始值应该是多少呢?应该不少同学疑惑,第一次还没买入呢,怎么初始化第二次买入呢?

第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后在买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。

所以第二次买入操作,初始化为:dp[0][3] = -prices[0];

第二次卖出初始化dp[0][4] = 0;

所以同理可以推出dp[0][j]当j为奇数的时候都初始化为 -prices[0]

4.确定遍历顺序

从前往后

//版本3,一维dp数组
class Solution{
	public int maxProfit(int k,int[] prices){
		if(prices.length==0){
			return 0;
		}
		if(k==0){
			return 0;
		}
		//其实就是123题的扩展,123题只用记录2次交易状态
		//这里记录k次交易的状态就行了
		//每次交易都有买入,卖出两个状态,所以要乘2
		int[] dp=new int[2*k];
		//按123题解题格式那样,做一个初始化
		for(int i=0;i<dp.length/2;i++){
			dp[i*2]=-prices[0];
		}
		for(int i=1;i<=prices.length;i++){
			dp[0]=Math.max(dp[0],-prices[i-1]);
			dp[1]=Math.max(dp[1],dp[0]+prices[i-1]);
			for(int j=2;j<dp.length;j+=2){
				dp[j]=Math.max(dp[j],dp[j-1]-prices[i-1]);
				dp[j+1]=Math.max(dp[j+1],dp[j]+prices[i-1]);
			}
			return dp[dp.length-1];
		}
	}
}

二十九、买卖股票的最佳时机,含冷冻期

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。

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

你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
示例:

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

1.确定dp数组的含义

java动态规划_第8张图片
dp[i][j],第i天状态为j,所剩的最多现金为dp[i][j]。

其实本题很多同学搞的比较懵,是因为出现冷冻期之后,状态其实是比较复杂度,例如今天买入股票、今天卖出股票、今天是冷冻期,都是不能操作股票的。

具体可以区分出如下四个状态:

状态一:持有股票状态(今天买入股票,或者是之前就买入了股票然后没有操作,一直持有)
不持有股票状态,这里就有两种卖出股票状态
状态二:保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。或者是前一天就是卖出股票状态,一直没操作)
状态三:今天卖出股票
状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!

j的状态为:

0:状态一
1:状态二
2:状态三
3:状态四
很多题解为什么讲的比较模糊,是因为把这四个状态合并成三个状态了,其实就是把状态二和状态四合并在一起了。

从代码上来看确实可以合并,但从逻辑上分析合并之后就很难理解了,所以我下面的讲解是按照这四个状态来的,把每一个状态分析清楚。

如果大家按照代码随想录顺序来刷的话,会发现 买卖股票最佳时机 1,2,3,4 的题目讲解中

动态规划:121.买卖股票的最佳时机(opens new window)
动态规划:122.买卖股票的最佳时机II(opens new window)
动态规划:123.买卖股票的最佳时机III(opens new window)
动态规划:188.买卖股票的最佳时机IV(opens new window)
「今天卖出股票」我是没有单独列出一个状态的归类为「不持有股票的状态」,而本题为什么要单独列出「今天卖出股票」 一个状态呢?

因为本题我们有冷冻期,而冷冻期的前一天,只能是 「今天卖出股票」状态,如果是 「不持有股票状态」那么就很模糊,因为不一定是 卖出股票的操作。

如果没有按照 代码随想录 顺序去刷的录友,可能看这里的讲解 会有点困惑,建议把代码随想录本篇之前股票内容的讲解都看一下,领会一下每天 状态的设置。

注意这里的每一个状态,例如状态一,是持有股票股票状态并不是说今天一定就买入股票,而是说保持买入股票的状态即:可能是前几天买入的,之后一直没操作,所以保持买入股票的状态。

2.递推公式

达到买入股票状态(状态一)即:dp[i][0],有两个具体操作:

操作一:前一天就是持有股票状态(状态一),dp[i][0] = dp[i - 1][0]
操作二:今天买入了,有两种情况
前一天是冷冻期(状态四),dp[i - 1][3] - prices[i]
前一天是保持卖出股票的状态(状态二),dp[i - 1][1] - prices[i]
那么dp[i][0] = max(dp[i - 1][0], dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]);

达到保持卖出股票状态(状态二)即:dp[i][1],有两个具体操作:

操作一:前一天就是状态二
操作二:前一天是冷冻期(状态四)
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);

达到今天就卖出股票状态(状态三),即:dp[i][2] ,只有一个操作:

昨天一定是持有股票状态(状态一),今天卖出

即:dp[i][2] = dp[i - 1][0] + prices[i];

达到冷冻期状态(状态四),即:dp[i][3],只有一个操作:

昨天卖出了股票(状态三)

dp[i][3] = dp[i - 1][2];

2.dp数组的初始化

dp[0][0]=-prices[0]
dp[0][1]=0
dp[0][2]=0
dp[0][3]=0

3.遍历顺序 从前到后

//一维数组优化
class Solution{
	public int maxProfit(int[] prices){
		int[] dp=new int[4];
		dp[0]=-prices[0];
		dp[1]=0;
		for(int i=1;i<=prices.length;i++){
		//使用临时变量来保存dp[0] dp[1]
		//因为马上dp[0]和dp[2]的数据会变
		int temp=dp[0];
		int temp1=dp[2];
		dp[0]=Math.max(dp[0],Math.max(dp[3],dp[1])-prices[i-1]);
		dp[1]=Math.max(dp[1],dp[3]);
		dp[2]=temp+prices[i-1];
		dp[3]=temp1;
		}
		return Math.max(dp[3],Math.max(dp[1],dp[2]));
	}
}

买卖股票最佳时机含手续费

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

示例 1:

输入: prices = [1, 3, 2, 8, 4, 9], fee = 2
输出: 8
解释: 能够达到的最大利润:

在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
注意:

0 < prices.length <= 50000.
0 < prices[i] < 50000.
0 <= fee < 50000.

//卖出时支付手续费
public int maxProfit(int[] prices,int fee){
	int len=prices.length;
	//0:持股(买入)
	//1:不持股(售出)
	//dp定义第i填持股/不持股 所得最多现金
	int[][] dp=new int[len][2];
	dp[0][0]=-prices[0];
	for(int i=1;i<len;i++){
		dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);
		dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]-fee,dp[i-1][1]);
	}
	return Math.max(dp[len-1][0],dp[len-1][1]);
}

三十、最长递增子序列

给定⼀个未经排序的整数数组,找到最长且 连续递增的⼦序列,并返回该序列的长度。
连续递增的⼦序列 可以由两个下标 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.确定dp[i]含义 :以nums[i]为结尾的最长递增子序列的长度

2.递推公式

dp[i]=max[dp[i]+1,dp[i]]

3.dp数组如何初始化

默认初始值都为1

4.遍历顺序:从前到尾

class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] dp = new int[nums.length];
        Arrays.fill(dp, 1);
        for (int i = 0; i < dp.length; i++) {
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
        }
        int res = 0;
        for (int i = 0; i < dp.length; i++) {
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

5.打印dp数组

三十一、最长连续递增子数列

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 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.确定dp[i]含义 :以下标i为结尾的连续递增的子序列长度为dp[i]。

2.递推公式

dp[i]=dp[i-1]+1

3.dp数组如何初始化

默认初始值都是1

4.遍历顺序:从前到尾

5.打印数组

public static int findLengthOfLCIS(int[] nums){
	int[] dp=new int[nums.length];
	for (int i=0;i<dp.length;i++){
		dp[i]=1;
	}
	int res=1;
	for (int i=0;i<nums.length;i++){
		if(nums[i+1]>nums[i]){
			dp[i+1]=dp[i]+1;
		}
		res=res>dp[i+1]?res:dp[i+1];
	}
	return res;
}

三十二、最长重复子数组

给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的⼦数组的长度。
⽰例:
输⼊:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出:3
解释:
长度最长的公共⼦数组是 [3, 2, 1] 。
提⽰:
1 <= len(A), len(B) <= 1000
0 <= A[i], B[i] < 100

1.确定dp[i][j]含义 :以i-1为结尾的nums1和以j-1为结尾的nums2的最长重复子数组(i-1和j-1方便后面初始化)

2.递推公式

if(nums1[i-1]==nums2[i-1])
dp[i][j]=dp[i-1][j-1]+1

3.dp数组如何初始化

dp[i][0]=0
dp[0][j]=0

4.遍历顺序:两层for,谁先都行

for(i=1;i<=nums1.size;i++)
for(j=1;j 顺便把最大值给求出来

5.打印dp数组

6.扩展dp定义

完整代码

// 版本一
class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int result = 0;
        int[][] dp = new int[nums1.length + 1][nums2.length + 1];
        
        for (int i = 1; i < nums1.length + 1; i++) {
            for (int j = 1; j < nums2.length + 1; j++) {
                if (nums1[i - 1] == nums2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                    result = Math.max(result, dp[i][j]);
                }
            }
        }
        
        return result;
    }
}

// 版本二: 滚动数组
class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int[] dp = new int[nums2.length + 1];
        int result = 0;

        for (int i = 1; i <= nums1.length; i++) {
            for (int j = nums2.length; j > 0; j--) {
                if (nums1[i - 1] == nums2[j - 1]) {
                    dp[j] = dp[j - 1] + 1;
                } else {
                    dp[j] = 0;
                }
                result = Math.max(result, dp[j]);
            }
        }
        return result;
    }
}

三十三、最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共⼦序列的长度。
⼀个字符串的 ⼦序列 是指这样⼀个新的字符串:它是由原字符串在不改变字符的相对顺序
的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的⼦序列,但 “aec” 不是 “abcde” 的⼦序列。两个字符串的「公共⼦序列」是这两个字符串所共同拥有的⼦序列。
若这两个字符串没有公共⼦序列,则返回 0。
⽰例 1:
输⼊:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共⼦序列是 “ace”,它的长度为 3。

1.确定dp[i][j]含义 :以[0,i-1]nums1和以[0,j-1]nums2的最长公共子序列的长

2.递推公式

if(nums1[i-1]==nums2[i-1])
dp[i][j]=dp[i-1][j-1]+1
else dp[i][j]=max(dp[i][j-1],dp[i-1][j])

3.dp数组如何初始化

第一行第一列初始化0

4.遍历顺序:两层for,从左到右,从上到下

for(i=1;i<=nums1.size;i++)
for(j=1;j<=nums2.size;j++)

4.完整代码

/*
	二维dp数组
*/
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int[][] dp = new int[text1.length() + 1][text2.length() + 1]; // 先对dp数组做初始化操作
        for (int i = 1 ; i <= text1.length() ; i++) {
            char char1 = text1.charAt(i - 1);
            for (int j = 1; j <= text2.length(); j++) {
                char char2 = text2.charAt(j - 1);
                if (char1 == char2) { // 开始列出状态转移方程
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[text1.length()][text2.length()];
    }
}



/**
	一维dp数组
*/
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int n1 = text1.length();
        int n2 = text2.length();

        // 多从二维dp数组过程分析  
        // 关键在于  如果记录  dp[i - 1][j - 1]
        // 因为 dp[i - 1][j - 1]    dp[j - 1]  <=>  dp[i][j - 1]
        int [] dp = new int[n2 + 1];

        for(int i = 1; i <= n1; i++){

            // 这里pre相当于 dp[i - 1][j - 1]
            int pre = dp[0];
            for(int j = 1; j <= n2; j++){

                //用于给pre赋值
                int cur = dp[j];
                if(text1.charAt(i - 1) == text2.charAt(j - 1)){
                    //这里pre相当于dp[i - 1][j - 1]   千万不能用dp[j - 1] !!
                    dp[j] = pre + 1;
                } else{
                    // dp[j]     相当于   dp[i - 1][j]
                    // dp[j - 1] 相当于   dp[i][j - 1]
                    dp[j] = Math.max(dp[j], dp[j - 1]);
                }

                //更新dp[i - 1][j - 1], 为下次使用做准备
                pre = cur;
            }
        }

        return dp[n2];
    }
}

三十九、编辑距离

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使⽤的最少操作数 。
你可以对⼀个单词进⾏如下三种操作:
插⼊⼀个字符
删除⼀个字符
替换⼀个字符
⽰例 1:
输⼊:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
⽰例 2:
输⼊:word1 = “intention”, word2 = “execution”
输出:5
解释:
intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插⼊ ‘u’)
提⽰:
0 <= word1.length, word2.length <= 500
word1 和 word2 由⼩写英⽂字母组成

1.确定dp[i][j]含义 :dp[i][j] 表⽰以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。

2.递推公式

在确定递推公式的时候,⾸先要考虑清楚编辑的⼏种操作,整理如下
java动态规划_第9张图片
if (word1[i - 1] == word2[j - 1]) 那么说明不⽤任何编辑,dp[i][j] 就应该是 dp[i- 1][j - 1],即dp[i][j] = dp[i - 1][j - 1]
if (word1[i - 1] != word2[j - 1]) 时取最⼩的,即:dp[i][j] =min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1

递归公式代码如下

if (word1[i - 1] == word2[j - 1]) {
 dp[i][j] = dp[i - 1][j - 1];
}
else {
 dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) +
1;
}

2.初始化dp数组

dp[i][j] 表⽰以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编
辑距离为dp[i][j]。
dp[i][0] :以下标i-1为结尾的字符串word1,和空字符串word2,最近编辑距离为dp[i[0]。
那么dp[i][0]就应该是i,对word1⾥的元素全部做删除操作,即:dp[i][0] = i;
同理dp[0][j] = j;

3.确定遍历顺序

从如下四个递推公式:
dp[i][j] = dp[i - 1][j - 1]
dp[i][j] = dp[i - 1][j - 1] + 1
dp[i][j] = dp[i][j - 1] + 1
dp[i][j] = dp[i - 1][j] + 1
可以看出dp[i][j]是依赖左⽅,上⽅和左上⽅元素的,如图java动态规划_第10张图片
所以在dp矩阵中⼀定是从左到右从上到下去遍历

4.完整代码

public int minDistance(String word1, String word2) {
    int m = word1.length();
    int n = word2.length();
    int[][] dp = new int[m + 1][n + 1];
    // 初始化
    for (int i = 1; i <= m; i++) {
        dp[i][0] =  i;
    }
    for (int j = 1; j <= n; j++) {
        dp[0][j] = j;
    }
    for (int i = 1; i <= m; i++) {
        for (int j = 1; j <= n; j++) {
            // 因为dp数组有效位从1开始
            // 所以当前遍历到的字符串的位置为i-1 | j-1
            if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1;
            }
        }
    }
    return dp[m][n];
}

你可能感兴趣的:(动态规划,算法,数据结构)