刷题笔记之动态规划(三)
打家劫舍
LeetCode 198. 打家劫舍
LeetCode题目链接
动态规划五部曲:
确定dp数组以及下标的含义
dp[i]: 考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]确定递推公式
分为两种情况:
不偷i房间:dp[i] = dp[i - 1],即保持上一次的状态。
偷i房间: dp[i] = dp[i - 2] + nums[i]。
那不偷i房间就一定会偷i - 1房间吗?
这里我们对dp数组的定义就很关键,考虑下标i(包括i)以内的房屋,并不是一定会偷i房间,到底偷不偷由递推公式推导得出。
所以状态转移方程为:dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
dp数组初始化
因为递推公式中有dp[i - 1]和dp[i - 2],所以应当初始化dp[0]和dp[1]。1
2dp[0] = nums[0];//考虑0号房间,最多可以偷窃的金额为nums[0]
dp[1] = Math.max(nums[0], nums[1]);//考虑1号(包括1号)以内的房屋,最多可以偷窃的金额选最大确定遍历顺序
由递推公式可知,dp[i]由dp[i - 1]和dp[i - 2]推导而来,应该从前往后遍历。1
2
3for(int i = 2; i <nums.length; i++){
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}打印dp数组
输入:[2,7,9,3,1],结果为:2 7 11 11 12
完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Solution {
public int rob(int[] nums) {
if (nums.length < 2) {
return nums[0];
}
int[] dp = new int[nums.length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
System.out.println(dp[i]);
}
return dp[nums.length - 1];
}
}
LeetCode 213. 打家劫舍 II
LeetCode题目链接
本题与198. 打家劫舍唯一不同的点是数组首尾相接,形成一个环,这意味着我们要在首尾之间进行取舍。
我们只需要分情况讨论即可。
- 情况一:考虑首尾都不偷
- 情况二:考虑包含首元素,不包含尾元素
- 情况三:考虑包含尾元素,不包含首元素
这里的关键点是考虑,并不是我把首/尾元素考虑进去,我就一定会偷!并且情况二和情况三都包含情况一,所以只实现情况二和情况三即可。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Solution {
public int rob(int[] nums) {
if (nums.length == 0) {
return 0;
}
if (nums.length == 1) {
return nums[0];
}
return Math.max(myRob(nums, 0, nums.length - 1), myRob(nums, 1, nums.length));
}
private int myRob(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;
}
}
LeetCode 337. 打家劫舍 III
LeetCode题目链接
本题与前面的dp题目都不同的点是这是一道树形dp,但本质上都是在考虑当前结点可不可取。
首先我们需要明确dp数组的定义:
dp[2]:记录当前结点偷与不偷两个状态得到的金钱,dp[0]代表不偷,dp[1]代表偷
我们以如图所示的二叉树举例:
如果不偷根节点,那就要考虑左右孩子要不要偷,即:root[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
如果偷根节点,那么左右孩子都不能偷,即:root[1] = root.val + left[0] + right[0]
;
从这里可以看出,无论偷不偷根节点,我们都要对其左右孩子结点操作,也就是说我们需要对二叉树进行后序遍历!
将其推广,则其递推公式为:
1 | //不偷当前结点,左右孩子在偷不偷之间做选择 |
由于是涉及递归,需要对终止条件和单层递归逻辑进行处理,我这里直接贴上代码,思路也比较简单:
1 | class Solution { |
买卖股票
这类题目我只会对二维数组版本进行总结,二维数组思路比较清晰,方便理解与总结,我会在完整代码部分附上空间优化版本。
LeetCode 121. 买卖股票的最佳时机
LeetCode题目链接
动态规划五部曲:
确定dp数组以及下标的含义
本题有两个状态,一个第i天持有股票的价值,另一个记录第i天买入还是卖出,所以需要定义一个二维数组
dp[i][0]: 第i天持有股票的最大价值
dp[i][1]: 第i天不持有股票的最大价值
这里的持有很关键,并不是说我第i天一定买入或卖出了股票得到了最大价值,它仅仅表示的是一种状态,我也可能第i - 1天已经卖出了。确定递推公式
如果第i天持有股票即dp[i][0],可以由两个状态推导而来
- 第i - 1天已经持有股票,那就维持原状,即dp[i - 1][0]
- 第i天买入股票,即-prices[0]
如果第i天不持有股票即dp[i][1],可以由两个状态推导而来: - 第i - 1天也不持有股票,维持原状,即dp[i - 1][1]
- 第i天卖出股票,那第i - 天就持有股票,将持有股票的价值和第i天卖出的价值相加,即dp[i - 1][0] + prices[i]
综上:1
2dp[i][0] = Math.max(dp[i - 1][0], -prices[i]);
dp[i][1] = Math.max(dp[i - 1], dp[i - 1][1] + prices[i]);
dp数组初始化
1
2dp[0][0] = -prices[0];//第0天持有股票
dp[0][1] = 0;//第0天不持有股票确定遍历顺序
由状态转移方程可知dp[i]由dp[i - 1]推导而来,所以从前往后遍历。打印dp数组
完整代码
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//二维数组版
class Solution {
public int maxProfit(int[] prices) {
if (prices.length < 2) {
return 0;
}
int len = prices.length;
int[][] dp = new int[len][2];
dp[0][0] = -prices[0];
dp[0][1] = 0;
for (int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i - 1][0], -prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
}
return Math.max(dp[len - 1][0], dp[len - 1][1]);
}
}
//空间优化版
class Solution {
public int maxProfit(int[] prices) {
if (prices.length <= 1) {
return 0;
}
int min = prices[0], max = 0;
// 第一种情况:最大收益不在最后一天产出,那么就在前i-1天中的某一天
// 第二种情况:最大收益在组后一天产出,记录前i-1天买入最小值求差值
for (int i = 1; i < prices.length; i++) {
max = Math.max(max, prices[i] - min);//max:前i天的最大收益 prices[i] - min:第i天的收益减去前i-1天买入的最小值
min = Math.min(min, prices[i]);//实时更新买入最小值
}
return max;
}
}
LeetCode 122. 买卖股票的最佳时机 II
LeetCode题目链接
121. 买卖股票的最佳时机只能购买一支股票,且不能在同一天卖出。
而本题可以买卖多支股票,且可以在同一天卖出,但在任何时候最多只能持有一支股票。
上一题持有股票状态下的递推公式为:dp[i][0] = Math.max(dp[i - 1][0], -prices[i]);
而本题可以购买两次,所以需要对其做一个微小的改动:dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
dp[i - 1][1] - prices[i]: 如果在第i天买入,那第i - i天就没有持有。
剩余部分照搬就好:
1 | //二位数组版本 |
LeetCode 123. 买卖股票的最佳时机 III
LeetCode题目链接
本题较之前的限制有最多可以完成两笔交易,且必须在再次购买前出售掉之前的股票。
我们之前定义了两种状态来表示买卖一次的状态,那现在可以买卖两次,我们只需要在原来的情况上再叠加一次就好:dp[i][0] = dp[i - 1][0]
: 什么都没做,维持原来状态dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i])
: 第一次持有dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i])
: 第一次卖出dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i])
: 第二次持有dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i])
: 第二次卖出
同时我们也要初始化四个值:
1 | int[][] dp = new int[len][5]; |
- 完整代码:
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//二维数组版本
class Solution {
public int maxProfit(int[] prices) {
if (prices.length < 2) {
return 0;
}
int len = prices.length;
int[][] dp = new int[len][5];
dp[0][0] = 0;//不操作
dp[0][1] = -prices[0];//第一次持有
dp[0][2] = 0;//第一次卖出
dp[0][3] = -prices[0];//第二次持有
dp[0][4] = 0;//第二次卖出
for (int i = 1; i < len; i++) {
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
}
return dp[len - 1][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];
}
}
LeetCode 188. 买卖股票的最佳时机 IV
LeetCode题目链接
与上一题唯一不同的点是股票可以买卖k次
我们知道,一次交易需要记录两种状态(持有/不持有),那么交易k次就要记录2k种状态,同时我们发现,在初始化时只有dp[i][#] #为基数是才初始为-prices[i]
1 | //二维数组版 |