Bootstrap

【1.2】动态规划-买卖股票的最佳时机

一、题目

        给定一个数组,它的第i个元素是一支给定的股票在第i天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成两笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

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


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


示例 3:
输入:p ri ce s = [ 7 , 6 , 4 , 3 , 1 ]
输出:0
解释:在这个情况下, 没有交易完成, 所以最大利润为 0。

示例 4:
输入:p ri ce s = [ 1 ]
输出:0


提示:
1)1 <= prices.length <= 10^5
2)0 <= prices[i] <= 10^5

二、求解思路

        这题让求的是股票最多交易两次的情况下所能获取的最大利润,我们定义dp[i][ j]表示 在j天结束之后最多交易i次所能获得的最大利润。很明显i只能是0,1,2。

表示要么没有交易,要么交易一次,要么交易两次,但不能超过两次,dp的定义如下

int[][] dp = new int[3][prices .length];

在第j天的时候我们可以选择不进行任何交易,那么,当天结束之后的利润也就是前一 天的利润。 在第j天的时候我们可以选择卖出一支股票,那么既然能卖出,说明在之前我们肯定买 过一支股票,那么当天结束之后的利润是

dp[i][j] = prices[j] - prices[k] + dp[i-1][k-1], k=[0..j-1] prices [ j] - prices [k]

表示的是在第k天买入一支股票,然后再第j天把它给卖掉所获 得的利润,其中k的范围是0…j -1。买一次卖一次算一次完整的交易,dp[i -1][k-1]表 示的是在前k-1天最多进行i -1次交易所获得的的最大利润。

所以第j天我们可以选择卖出一支股票也可以选择不进行任何操作,取最大值即可,所以递推公式如下

dp[i][j] = Math.max(dp[i][j-1], prices[j] - prices[k] + dp[i - 1][k - 1]);

算法步骤

  1. 定义状态

    • dp[i][j] 表示在第 j 天结束时,最多进行 i 次交易所能获得的最大利润。

  2. 初始化

    • dp[0][j] 表示在第 j 天结束时,没有进行任何交易的最大利润,显然为 0。

    • dp[i][0] 表示在第 0 天结束时,最多进行 i 次交易的最大利润,显然为 0。

  3. 状态转移方程

    • 对于 dp[i][j],我们有两种选择:

      1. 不进行任何交易,即 dp[i][j] = dp[i][j-1]

      2. 进行一次交易,即在第 k 天买入,在第 j 天卖出,dp[i][j] = prices[j] - prices[k] + dp[i-1][k-1],其中 k 的范围是 0j-1

    • 因此,递推公式为:

      dp[i][j] = Math.max(dp[i][j-1], prices[j] - prices[k] + dp[i-1][k-1]);
  4. 最终结果

    • 最终结果为 dp[2][prices.length - 1],即在最后一天结束时,最多进行两次交易的最大利润。

三、代码实现

C语言实现

#include <stdio.h>
#include <stdlib.h>

// 函数用于计算最大利润
int maxProfit(int* prices, int pricesSize) {
    // 如果价格数组为空,则直接返回0利润
    if (pricesSize == 0) return 0;

    // 动态分配内存,创建一个二维数组dp,用于存储中间计算结果
    int** dp = (int**)malloc(3 * sizeof(int*));
    for (int i = 0; i < 3; ++i) {
        dp[i] = (int*)malloc(pricesSize * sizeof(int));
        // 初始化dp数组的每个元素为0
        for (int j = 0; j < pricesSize; ++j) {
            dp[i][j] = 0;
        }
    }

    // 动态规划计算最大利润
    for (int i = 1; i <= 2; ++i) {
        int maxDiff = -prices[0]; // 初始化最大差价为第一天的价格的负值
        for (int j = 1; j < pricesSize; ++j) {
            // 更新maxDiff为之前的maxDiff和第j天之前进行i-1次交易的最大利润减去第j-1天的价格的较大者
            maxDiff = (maxDiff > dp[i-1][j-1] - prices[j-1]) ? maxDiff : dp[i-1][j-1] - prices[j-1];
            // 更新第i次交易第j天的最大利润为前一天的最大利润和第j天价格加上maxDiff的较大者
            dp[i][j] = (dp[i][j-1] > prices[j] + maxDiff) ? dp[i][j-1] : prices[j] + maxDiff;
        }
    }

    // 结果为完成两次交易后的最后一天的最大利润
    int result = dp[2][pricesSize-1];

    // 释放动态分配的内存
    for (int i = 0; i < 3; ++i) {
        free(dp[i]);
    }
    free(dp);

    return result;
}

// 主函数
int main() {
    // 给定的股票价格数组
    int prices[] = {3, 3, 5, 0, 0, 3, 1, 4};
    // 计算数组的长度
    int pricesSize = sizeof(prices) / sizeof(prices[0]);
    // 调用maxProfit函数并打印最大利润
    printf("Maximum profit: %d\n", maxProfit(prices, pricesSize));
    return 0;
}

C++代码实现

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// 函数用于计算最大利润,参数是一个包含股票价格历史的整数向量
int maxProfit(vector<int>& prices) {
    // 如果价格向量为空,直接返回0利润
    if (prices.empty()) return 0;

    int n = prices.size(); // 获取价格向量的大小
    // 创建一个二维动态数组dp,用于存储中间计算结果,初始化所有值为0
    vector<vector<int>> dp(3, vector<int>(n, 0));

    // 动态规划计算最大利润
    for (int i = 1; i <= 2; ++i) {
        int maxDiff = -prices[0]; // 初始化最大差价为第一天的价格的负值
        for (int j = 1; j < n; ++j) {
            // 更新maxDiff为之前的maxDiff和第j天之前进行i-1次交易的最大利润减去第j-1天的价格的较大者
            maxDiff = max(maxDiff, dp[i-1][j-1] - prices[j-1]);
            // 更新第i次交易第j天的最大利润为前一天的最大利润和第j天价格加上maxDiff的较大者
            dp[i][j] = max(dp[i][j-1], prices[j] + maxDiff);
        }
    }

    // 结果为完成两次交易后的最后一天的最大利润
    return dp[2][n-1];
}

// 主函数
int main() {
    // 给定的股票价格向量
    vector<int> prices = {3, 3, 5, 0, 0, 3, 1, 4};
    // 调用maxProfit函数并打印最大利润
    cout << "Maximum profit: " << maxProfit(prices) << endl;
    return 0;
}
  1. 初始化

    • dp 是一个二维数组,dp[i][j] 表示在第 j 天结束时,最多进行 i 次交易所能获得的最大利润。

    • maxDiff 用于记录在第 j 天之前,最多进行 i-1 次交易的最大利润减去第 j-1 天的价格。

  2. 状态转移

    • 对于每个 i(1 和 2),我们计算 maxDiff 并更新 dp[i][j]

    • maxDiff 的更新公式是 maxDiff = max(maxDiff, dp[i-1][j-1] - prices[j-1])

    • dp[i][j] 的更新公式是 dp[i][j] = max(dp[i][j-1], prices[j] + maxDiff)

  3. 返回结果

    • 最终结果是 dp[2][n-1],即在最后一天结束时,最多进行两次交易的最大利润。

四、另外解法

        动态规划相关的题怎么解,主要看状态的定义。上面我们定义的是二维数组,这里我们还可以定义一个三维数组。
        定义dp[i][ j][k]表示在第i天交易结束后,最多进行j次交易所获得的最大利润。注意这里的k要么是0,要么是1。0表示手里没有股票,1表示手里有一支股票。

dp[i][ j][0]:表示第i天交易结束之后,最多进行j次交易,并且手里没有股票的最大利润。
dp[i][ j][1]:表示第i天交易结束之后,最多进行j次交易,并且手里持有股票的最大利润。

那么在当天结束之后我们会有6种状态
1、没有进行过任何交易,利润永远为0
 dp[i][0][0]=0

2、卖出过一次股票(完成一次交易),但目前手上没有股票:可能是今天卖出,也可
能是之前卖的(注意:买一次卖一次才能算一次完整的交易)
dp[i][1][0]=max(dp[i-1][0][1]+prices[i],dp[i-1][1][0])

3、卖出过两次股票(完成两次交易),但目前手上没有股票:也可能是今天卖的,也可能是之前卖的。
dp[i][2][0]=max(dp[i-1][1][1]+prices[i],dp[i-1][2][0])

4、没有卖出过任何股票,但目前手上持有股票:可能是今天持有的,也可能是之前持有的
dp[i][0][1]=max(dp[i-1][0][0]-prices[i],dp[i-1][0][1])

5,卖出过一次股票(完成一次交易),但目前手上持有股票:可能是今天持有的,也
可能是之前持有的
dp[i][1][1]=max(dp[i-1][1][0]-prices[i],dp[i-1][1][1])

6,卖出过两次次股票(完成两次交易),但目前手上持有股票:由于最多交易2次,
这种情况是无效的
dp[i][2][1]:无效

        有了上面的递推公式,我们再来看一下base case,第一天的时候我们要么买一支股
票,要么什么也不做。

C++代码实现

#include <vector>
#include <algorithm>
#include <climits>

// 定义一个名为Solution的类
class Solution {
public:
    // 类的公有成员函数,用于计算最大利润
    int maxProfit(std::vector<int>& prices) {
        int n = prices.size(); // 获取价格数组的长度
        // 如果数组为空,则返回0
        if (n == 0) return 0;

        // 使用三维向量dp来保存动态规划的状态
        // dp[i][j][k]代表第i天进行j次交易,并且是否持有股票(k为1表示持有,0表示不持有)
        std::vector<std::vector<std::vector<int>>> dp(n, std::vector<std::vector<int>>(3, std::vector<int>(2, 0)));

        // 初始化第一天的状态
        dp[0][0][0] = 0; // 第一天没有进行任何交易,也没有持有股票
        dp[0][0][1] = -prices[0]; // 第一天买入股票
        // 将不可能的情况初始化为一个很小的值,表示这些状态不会被选择
        dp[0][1][0] = INT_MIN / 2; // 第一天不可能已经卖出股票
        dp[0][1][1] = INT_MIN / 2; // 第一天不可能已经进行了一次买入和一次卖出
        dp[0][2][0] = INT_MIN / 2; // 第一天不可能已经进行了两次卖出
        dp[0][2][1] = INT_MIN / 2; // 第一天不可能已经进行了两次买入和卖出

        // 遍历每一天,更新dp数组
        for (int i = 1; i < n; ++i) {
            // 更新当天持有股票的状态,考虑前一天已经持有或者当天买入
            dp[i][0][1] = std::max(dp[i - 1][0][0] - prices[i], dp[i - 1][0][1]);
            // 更新当天不持有股票并且进行了一次交易的状态,考虑前一天持有股票并在当天卖出或者前一天已经进行了一次交易
            dp[i][1][0] = std::max(dp[i - 1][0][1] + prices[i], dp[i - 1][1][0]);
            // 更新当天持有股票并且进行了一次交易的状态,考虑前一天已经进行了一次交易并在当天买入或者前一天持有股票并且进行了一次交易
            dp[i][1][1] = std::max(dp[i - 1][1][0] - prices[i], dp[i - 1][1][1]);
            // 更新当天不持有股票并且进行了两次交易的状态,考虑前一天持有股票并在当天卖出或者前一天已经进行了两次交易
            dp[i][2][0] = std::max(dp[i - 1][1][1] + prices[i], dp[i - 1][2][0]);
            // dp[i][2][1] 是不可能的状态,因为我们只能进行两次交易,所以不需要更新
        }

        // 返回最终的最大利润,可能是没有进行交易、进行了一次交易或者进行了两次交易
        return std::max({dp[n - 1][0][0], dp[n - 1][1][0], dp[n - 1][2][0]});
    }
};
上面的三维数组我们还可以使用4个变量来表示,下面有详细注释,具体可以看下

#include <vector>
#include <algorithm>
#include <climits>

// 定义一个名为 Solution 的类,用于解决最大利润问题
class Solution {
public:
    // 定义一个函数 maxProfit,它接受一个整数类型的 vector 引用,表示每天的股票价格
    int maxProfit(std::vector<int>& prices) {
        // 初始化第一次买入股票的最大利润为 INT_MIN / 2,以避免溢出
        int buy1 = INT_MIN / 2;
        // 初始化第一次卖出股票的最大利润为 0
        int sell1 = 0;
        // 初始化第二次买入股票的最大利润为 INT_MIN / 2,以避免溢出
        int buy2 = INT_MIN / 2;
        // 初始化第二次卖出股票的最大利润为 0
        int sell2 = 0;

        // 遍历每一天的股票价格
        for (int price : prices) {
            // 更新第二次卖出的最大利润,取决于在这一天卖出或保持之前的卖出状态
            sell2 = std::max(buy2 + price, sell2);
            // 更新第二次买入的最大利润,取决于在这一天买入或保持之前的买入状态
            buy2 = std::max(sell1 - price, buy2);
            // 更新第一次卖出的最大利润,取决于在这一天卖出或保持之前的卖出状态
            sell1 = std::max(buy1 + price, sell1);
            // 更新第一次买入的最大利润,取决于在这一天买入或保持之前的买入状态
            buy1 = std::max(-price, buy1);
        }

        // 最终返回的是最大利润,可能是没有交易,进行了一次交易,或者进行了两次交易的最大值
        return std::max(0, std::max(sell1, sell2));
    }
};

;