算法训练营(day48-51)

动态规划理论基础

动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。

举个例子:有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

动态规划中 dp[j] 是由 dp[j-weight[i]] 推导出来的,然后取 max(dp[j], dp[j - weight[i]] + value[i])

动态规划的解题步骤

  1. 确定 dp 数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp 数组如何初始化
  4. 确定遍历顺序
  5. 举例推导 dp 数组

121. 买卖股票的最佳时机

题目链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/description/

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

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

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

解题思路

解题过程:动态规划

按照动规五部曲来分析:

  1. 确定dp数组(dp table)以及下标的含义

​ dp[j]表示容量为j的背包,最多可以背最大重量为dp[j]

本题的 dp[i]:第i天持有股票所得最多现金

  1. 确定递推公式
  • 如果第 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]) ;

  1. dp数组的初始化

递推公式的基础就是dp[0] 和 dp[1]

1
2
3
int[] dp = new int[2];
dp[0] = -prices[0]; //买入
dp[1] = 0; //卖出
  1. 确定遍历顺序

dp[i] 是根据 dp[i - 1] 推导出来的,那么一定是从前到后遍历!

1
2
3
4
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]);
}
  1. 推导dp数组

详细代码

动态规划:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public int maxProfit(int[] prices) {
int[] dp = new int[2];
dp[0] = -prices[0]; //买入
dp[1] = 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]);
}
return dp[1];
}
}

贪心:

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public int maxProfit(int[] prices) {
int low = Integer.MAX_VALUE;
int res = 0;
for(int i = 0; i < prices.length; i++){
low = Math.min(low, prices[i]);
res = Math.max(prices[i] - low, res);
}
return res;
}
}

122. 买卖股票的最佳时机 II

题目链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/description/

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润

解题思路

解题过程:动态规划

按照动规五部曲来分析:

  1. 确定dp数组(dp table)以及下标的含义

​ dp[j]表示容量为j的背包,最多可以背最大重量为dp[j]

本题的 dp[i]:第i天持有股票所得最多现金

  1. 确定递推公式
  • 如果第 i 天持有股票即 dp[i][0] , 那么可以由两个状态推出来

    • i-1 天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]

    • i 天买入股票,所得现金就是买入今天的股票后所得现金即: dp[i - 1][1] - prices[i]

​ 那么 dp[i][0] 应该选所得现金最大的,所以 dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] -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]) ;

  1. dp数组的初始化

递推公式的基础就是dp[0] 和 dp[1]

1
2
3
int[][] dp = new int[prices.length][2];
dp[0][0] = -prices[0]; //买入
dp[0][1] = 0; //卖出
  1. 确定遍历顺序

dp[i] 是根据 dp[i - 1] 推导出来的,那么一定是从前到后遍历!

1
2
3
4
for(int i = 1; i < prices.length; 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]);
}
  1. 推导dp数组

详细代码

动态规划:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public int maxProfit(int[] prices) {
int[][] dp = new int[prices.length][2];
dp[0][0] = -prices[0];
dp[0][1] = 0;

for(int i = 1; i < prices.length; 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]);
}
return dp[prices.length - 1][1];
}
}

递归:

1
2
3
4
5
6
7
8
9
class Solution {
public int maxProfit(int[] prices) {
int res = 0;
for(int i = 1; i < prices.length; i++){
res += Math.max(prices[i] - prices[i - 1], 0);
}
return res;
}
}

123. 买卖股票的最佳时机 III

题目链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/description/

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

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

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

解题思路

解题过程:动态规划

按照动规五部曲来分析:

  1. 确定dp数组(dp table)以及下标的含义

​ dp[j]表示容量为j的背包,最多可以背最大重量为dp[j]

本题的 dp[i]:第i次买卖股票所得最多现金

  1. 确定递推公式
  • 如果只有1次 买卖股票 即只存在 dp[0]dp[1] , 那么可以由两个状态推出来

    • dp[0] 表示买入股票:

      • 前一天买入,-price[i - 1]

      • 不做任何操作,保留 dp[0]

    • dp[1] 表示卖出股票:

      • 前一天卖出,dp[0] + price[i - 1]
      • 不做任何操作,保留 dp[1]
  • 如果存在 2次买卖股票 即存在四种情况 dp[0]、dp[1]、dp[2] 和 dp[3] , 也可以由 一次买卖股票再推出 两个状态:

    • dp[2] 表示 第二次 买入股票:

      • 前一天买入,dp[1] - price[i - 1]

      • 不做任何操作,保留 dp[2]

    • dp[3] 表示 第二次 卖出股票:

      • 前一天卖出 dp[2] + price[i - 1]
      • 不做任何操作,保留 dp[3]
  1. dp数组的初始化

递推公式的基础就是四个状态:dp[0] 、 dp[1]、dp[2] 和 dp[3]

1
2
3
4
5
int[] dp = new int[4];
dp[0] = -prices[0];
dp[1] = 0;
dp[2] = -prices[0];
dp[3] = 0;
  1. 确定遍历顺序

dp[i] 是根据 dp[i - 1] 推导出来的,那么一定是从前到后遍历!

1
2
3
4
5
6
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]);
}
  1. 推导dp数组

详细代码

动态规划:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int maxProfit(int[] prices) {
int[] dp = new int[4];
dp[0] = -prices[0];
dp[1] = 0;
dp[2] = -prices[0];
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];
}
}

188. 买卖股票的最佳时机 IV

题目链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/description/

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

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

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

解题思路

解题过程:动态规划

按照动规五部曲来分析:

  1. 确定dp数组(dp table)以及下标的含义

​ dp[j]表示容量为j的背包,最多可以背最大重量为dp[j]

本题的 dp[2i]:第k(k = 2i)次买卖股票所得最多现金

  1. 确定递推公式

    如果 k < 1,直接返回0就行

  • 第1次 买卖股票 即只存在 dp[0]dp[1] , 那么可以由两个状态推出来

    • dp[0] 表示买入股票:
      • 前一天买入,-price[i - 1]

      • 不做任何操作,保留 dp[0]

    • dp[1] 表示卖出股票:
      • 前一天卖出,dp[0] + price[i - 1]
      • 不做任何操作,保留 dp[1]
  • 后面的k次买卖股票,都由 一次买卖股票再推出 两个状态:for(int j = 2; j < dp.length; j += 2){}

    • dp[j] 表示买入股票
      • 前一天买入,dp[j - 1] - price[i - 1]
      • 不做任何操作,保留 dp[j]
    • dp[j + 1] 表示卖出股票:
      • 前一天卖出,dp[j] + price[i - 1]
      • 不做任何操作,保留 dp[j + 1]
  1. dp数组的初始化

递推公式的基础就是dp[0] 和 dp[1]

1
2
3
4
5
6
7
8
9
10
   int[] dp = new int[2 * k];
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]);
...
}
  1. 确定遍历顺序

dp[i] 是根据 dp[i - 1] 推导出来的,那么一定是从前到后遍历!

1
2
3
4
5
6
7
8
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]);
}
}
  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
class Solution {
public int maxProfit(int k, int[] prices) {
if(prices.length == 0){
return 0;
}
if(k == 0){
return 0;
}

int[] dp = new int[2 * k];
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];
}
}

309. 最佳买卖股票时机含冷冻期

题目链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/description/

给定一个整数数组prices,其中第 prices[i] 表示第 *i* 天的股票价格 。

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

  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

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

解题思路

解题过程:动态规划

按照动规五部曲来分析:

  1. 确定dp数组(dp table)以及下标的含义

​ dp[j]表示容量为j的背包,最多可以背最大重量为dp[j]

本题的 dp[i][j]:第i天状态为j,持有股票所得最多现金

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

  • 不持有股票状态
    • 状态一:今天是冷冻期
    • 状态二:过了冷冻期
    • 状态三:今天卖出股票(明天是冷冻期)
  • 持有股票状态
    • 状态四:保持买入股票的状态
      • 今天买入股票
      • 保持股票买入状态

img

其中,状态二和状态四可以是同一天,所以主要是三种情况

  1. 确定递推公式
  • 不持有股票dp[i][0] , 那么可以由两个状态推出来

  • i-1 天就不持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]

  • i-1 天卖出股票,所得现金就是卖出今天的股票后所得现金即: dp[i - 1][1] + prices[i]

​ 那么 dp[i][0] 应该选所得现金最大的,所以 dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);

  • 持有股票dp[i][1] , 也可以由两个状态推出来

  • i-1 天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即: dp[i - 1][1]

  • i-2 天卖出了股票(第 i - 1天是冷冻期),第 i 天要买入股票,所得现金就是按照第 i -2股票价格卖出后所得现金 减去当天的股票金额即: dp[i - 2][0] - prices[i]

​ 同样 dp[i][1] 取最大的,**dp[i][1] = Math.max(dp[i - 1][1], dp[i - 2][0] - prices[i]) **;

  1. dp数组的初始化

​ **递推公式的基础就是第一次买卖股票 dp[0][0]dp[0][1] 和 第一次过冷冻期后的 买卖股票 dp[1][0]dp[1][1] **

1
2
3
4
5
int[][] dp = new int[prices.length][2];
dp[0][0] = 0; //不操作(冷冻期)
dp[0][1] = -prices[0]; //买入
dp[1][0] = Math.max(dp[0][0], dp[0][1] + prices[1]); //卖出或不操作(冷冻期)
dp[1][1] = Math.max(dp[0][1], - prices[1]); //买入或冷冻期后买入
  1. 确定遍历顺序

dp[i] 是根据 dp[i - 1] 推导出来的,那么一定是从前到后遍历!

1
2
3
4
for(int i = 2; i < prices.length; 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 - 2][0] - prices[i]);
}
  1. 推导dp数组

详细代码

解法一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public int maxProfit(int[] prices) {
if(prices == null || prices.length < 2){
return 0;
}
int[][] dp = new int[prices.length][2];

dp[0][0] = 0; //不操作(冷冻期)
dp[0][1] = -prices[0]; //买入
dp[1][0] = Math.max(dp[0][0], dp[0][1] + prices[1]); //卖出或不操作(冷冻期)
dp[1][1] = Math.max(dp[0][1], - prices[1]); //买入或冷冻期后买入

for(int i = 2; i < prices.length; 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 - 2][0] - prices[i]);
}
return dp[prices.length - 1][0];
}
}

解法二:

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public int maxProfit(int[] prices) {
int[][] dp = new int[prices.length + 1][2];
dp[1][0] = -prices[0]; //买入

for(int i = 2; i <= prices.length; i++){
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 2][1] - prices[i - 1]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i - 1]);
}
return dp[prices.length][1];
}
}

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

题目链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/description/

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

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

返回获得利润的最大值。

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

解题思路

解题过程:动态规划

按照动规五部曲来分析:

  1. 确定dp数组(dp table)以及下标的含义

​ dp[j]表示容量为j的背包,最多可以背最大重量为dp[j]

本题的 dp[i]:第i天持有股票所得最多现金

  1. 确定递推公式
  • 如果第 i不持有股票dp[i][0] , 那么可以由两个状态推出来

    • i-1 天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][0]

    • i 天卖出股票,所得现金就是卖出今天的股票后所得现金即: dp[i - 1][1] + prices[i]

​ 那么 dp[i][0] 应该选所得现金最大的,所以 dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);

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

    • i-1 天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即: dp[i - 1][1]

    • i 天买入股票,所得现金就是按照今天股票价格买入后再减去手续费后所得现金即: dp[i - 1][0] - prices[i] - fee

​ 同样 dp[i][1] 取最大的,**dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee) **;

  1. dp数组的初始化

递推公式的基础就是dp[0] 和 dp[1]

1
2
3
int[][] dp = new int[prices.length][2];
dp[0][0] = 0; //不持有股票/卖出
dp[0][1] = -prices[0] - fee; //买入股票
  1. 确定遍历顺序

dp[i] 是根据 dp[i - 1] 推导出来的,那么一定是从前到后遍历!

1
2
3
4
for(int i = 1; i < prices.length; 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);
}
  1. 推导dp数组

详细代码

动态规划:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public int maxProfit(int[] prices, int fee) {
if(prices == null || prices.length == 0){
return 0;
}
int[][] dp = new int[prices.length][2];
dp[0][0] = 0; //不持有股票/卖出
dp[0][1] = -prices[0] - fee; //买入股票

for(int i = 1; i < prices.length; 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);
}
return dp[prices.length - 1][0];
}
}

递归:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public int maxProfit(int[] prices, int fee) {
int sum = 0;
int buy = prices[0] + fee;

for(int p : prices){
if(p + fee < buy){
buy = p + fee;
}else if(p > buy){
sum += p - buy;
buy = p;
}
}
return sum;
}
}