代码随想录第五十天 | 动态规划 买卖股票:最多买卖两次股票的最佳时机(123);拓展至k维:最多买卖k次股票的最佳时机(188)

1、最多买卖两次股票的最佳时机

1.1 leetcode 123:买卖股票的最佳时机III

第一遍代码,没有思路,不知道怎么控制最多买两次

代码随想录思路:
使用动态规划dp数组 完成 最多买两次的控制,至于如何控制,跟 leetcode 112:买卖股票的最佳时机II 控制买入卖出一样同一天可能有多少状态dp数组第二维有多大每一个该维的格子对应一个状态
关键在于至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖,据此可以确定状态 以及 状态的数量

接来下用动态规划五部曲详细分析一下:
1、确定dp数组以及下标的含义
买卖股票问题对应的 状态转换 的过程中,上一个状态有多种可能性来源),同样当前状态也有很多种可能性
一天一共就有五个状态
(1)没有操作 (其实我们也可以不设置这个状态)
(2)第一次持有股票
(3)第一次不持有股票
(4)第二次持有股票
(5)第二次不持有股票
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] 延续买入股票的这个状态

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])
从这里看出来刚开始第一次卖出去的时候比的是负得少,所以对后面初始化注意卖出去的时候初值不是0

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

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天做第一次卖出的操作,这个初始值应该是多少呢?
此时还没有买入,怎么就卖出呢? 其实大家可以理解当天买入,当天卖出(也可以想着是正数比大,所以初值为0就可以),所以dp[0][2] = 0;

第0天第二次买入操作,初始值应该是多少呢?第一次还没买入呢,怎么初始化第二次买入呢?
第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后再买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少
所以第二次买入操作,初始化为:dp[0][3] = -prices[0];

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

4、确定遍历顺序
递归公式其实已经可以看出,一定是从前向后遍历,因为dp[i],依靠dp[i - 1]的数值

5、举例推导dp数组
以输入[1,2,3,4,5]为例
代码随想录第五十天 | 动态规划 买卖股票:最多买卖两次股票的最佳时机(123);拓展至k维:最多买卖k次股票的最佳时机(188)_第1张图片
红色框为最后两次卖出的状态

现在最大的时候一定是卖出的状态,而两次卖出的状态现金最大一定是最后一次卖出。如果第一次卖出已经是最大值了,那么我们可以在当天立刻买入再立刻卖出。所以dp[4][4]已经包含了dp[4][2]的情况。也就是说第二次卖出手里所剩的钱一定是最多的,相当于dp[i][4] 跟 dp[i][2] 重复操作了,都买了卖了同一只股票
所以最终最大利润是dp[4][4]

根据思路实现代码:

dp[i][0] 与 dp[i][2] 的同样是买入式子的差别体现了不重复买 / 重复买的差别
dp[i][1] 与 dp[i][3] 的同样是卖出式子的相似体现了不重复卖 / 重复卖的相似
包括初始化也可以得到多次买入卖出 与 单次买入卖出的相似性

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        vector<vector<int>> dp(prices.size(), vector<int> (4));
        dp[0][0] = -prices[0];
        dp[0][1] = 0;
        dp[0][2] = -prices[0];
        dp[0][3] = 0;
        for(int i = 1; i < prices.size(); i++) {
            dp[i][0] = max(dp[i - 1][0], -prices[i]);
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]);
            dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] - prices[i]);
            dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] + prices[i]);
            /*
            dp[i][0] 与 dp[i][2] 的同样是买入式子的差别体现了不重复买 / 重复买的差别
            dp[i][1] 与 dp[i][3] 的同样是卖出式子的相似体现了不重复卖 / 重复卖的相似
            包括初始化也可以得到多次买入卖出 与 单次买入卖出的相似性
            */
        }
        return dp[prices.size() - 1][3];
    }
};

代码随想录实现代码:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (prices.size() == 0) return 0;
        vector<vector<int>> dp(prices.size(), vector<int>(5, 0));
        dp[0][1] = -prices[0];
        dp[0][3] = -prices[0];
        for (int i = 1; i < prices.size(); i++) {
            dp[i][0] = dp[i - 1][0];
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
            dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
            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]);
        }
        return dp[prices.size() - 1][4];
    }
};

其实我们可以不设置,‘0. 没有操作’ 这个状态,因为没有操作,手上的现金自然就是0
递推式中发现,当前状态的来源 只和 上一次的状态 有关,所以可以去掉一个维度
改进代码:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        if (prices.size() == 0) return 0;
        vector<int> dp(5, 0);
        dp[1] = -prices[0];
        dp[3] = -prices[0];
        for (int i = 1; i < prices.size(); i++) {
            dp[1] = max(dp[1], dp[0] - prices[i]);
            dp[2] = max(dp[2], dp[1] + prices[i]);
            dp[3] = max(dp[3], dp[2] - prices[i]);
            dp[4] = max(dp[4], dp[3] + prices[i]);
        }
        return dp[4];
    }
};

2、最多买卖k次股票的最佳时机

2.1 leetcode 188:买卖股票的最佳时机IV

上一道题 leetcode 123:买卖股票的最佳时机III 的2换成k就行,思路一致运用上一题自己实现代码的注释部分:
dp[i][0] 与 dp[i][2] 的同样是买入式子的差别体现了不重复买 / 重复买的差别
dp[i][1] 与 dp[i][3] 的同样是卖出式子的相似体现了不重复卖 / 重复卖的相似
包括初始化也可以得到多次买入卖出 与 单次买入卖出的相似性

8把这个拓展至k维就ac了

第一遍代码:

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        //把上一道题的2换成k就行,思路一致,运用上一题自己实现代码的注释部分
        vector<vector<int>> dp(prices.size(), vector<int>(k * 2));
        for(int i = 0; i < k; i++) {
            dp[0][i * 2] = -prices[0];
            dp[0][i * 2 + 1] = 0;
        }
        for(int i = 1; i < prices.size(); i++) {
            dp[i][0] = max(dp[i - 1][0], -prices[i]);
            for(int j = 1; j < k * 2; j++) {
                if(j % 2 == 0) {
                    dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] - prices[i]);
                }
                else {
                    dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1] + prices[i]);
                }
            }
        }
        return dp[prices.size() - 1][2*k - 1];
    }
};

代码随想录思路
这道题目可以说是动态规划:123.买卖股票的最佳时机III 的进阶版,这里要求至多有k次交易

动规五部曲,分析如下:
1、确定dp数组以及下标的含义
在动态规划:123.买卖股票的最佳时机III 中,我是定义了一个二维dp数组,本题其实依然可以用一个二维dp数组,使用二维数组 dp[i][j] :第i天的状态为j,所剩下的最大现金是dp[i][j]

j的状态表示为:
0 表示不操作
1 第一次买入
2 第一次卖出
3 第二次买入
4 第二次卖出


大家应该发现规律了吧 ,除了0以外偶数就是卖出奇数就是买入(如果按第一次代码思路,没有不操作的维数的话结论相反)

题目要求是至多有K笔交易,那么j的范围就定义为 2 * k + 1 就可以了
所以二维dp数组的C++定义为:

vector<vector<int>> dp(prices.size(), vector<int>(2 * k + 1, 0));

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])

同理可以类比剩下的状态,代码如下:

for (int j = 0; j < 2 * k - 1; j += 2) {
    dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
    dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
}

2、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]

代码如下:

for (int j = 1; j < 2 * k; j += 2) {
    dp[0][j] = -prices[0];
}

初始化的地方同样要类比j为偶数是卖、奇数是买的状态

4、确定遍历顺序
递归公式其实已经可以看出,一定是从前向后遍历因为dp[i],依靠dp[i - 1]的数值

5、举例推导dp数组
以输入[1,2,3,4,5],k=2为例
代码随想录第五十天 | 动态规划 买卖股票:最多买卖两次股票的最佳时机(123);拓展至k维:最多买卖k次股票的最佳时机(188)_第2张图片
最后一次卖出,一定是利润最大的,dp[prices.size() - 1][2 * k]即红色部分就是最后求解
代码随想录代码:

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {

        if (prices.size() == 0) return 0;
        vector<vector<int>> dp(prices.size(), vector<int>(2 * k + 1, 0));
        for (int j = 1; j < 2 * k; j += 2) {
            dp[0][j] = -prices[0];
        }
        for (int i = 1;i < prices.size(); i++) {
            for (int j = 0; j < 2 * k - 1; j += 2) {
                dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]);
                dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]);
            }
        }
        return dp[prices.size() - 1][2 * k];
    }
};

时间复杂度: O(n * k),其中 n 为 prices 的长度
空间复杂度: O(n * k)

你可能感兴趣的:(leetcode,c++,动态规划,算法,c++,leetcode)