【leetcode买卖股票系列问题】多次买卖/手续费/冻结期

一、目录

一、目录

二、股票系列问题

1.买卖股票的最佳时机(121题)

1.1.题目

1.2.思路

1.3.代码实现(1种) 

2.买卖股票的最佳时机II(122题)

2.1.题目

 2.2.思路

2.3.代码实现(3种)

3.买卖股票的最佳时机III(123题)

3.1.题目

3.2.思路

3.3.代码实现(2种) 

4.买卖股票的最佳时机IV(188题)

4.1.题目

4.2.思路

 4.3.代码实现(1种)

5.买卖股票的最佳时机含冷冻期(309题)

5.1.题目

5.2.思路

5.3.代码实现(2种)

6.买卖股票的最佳时机含手续费(714题)

6.1.题目

6.2.思路

6.3.代码实现(2种) 

7.股票的最大利润(剑指Offer63题)

三、总结


二、股票系列问题

1.买卖股票的最佳时机(121题)

1.1.题目

【leetcode买卖股票系列问题】多次买卖/手续费/冻结期_第1张图片

1.2.思路

dp:根据题目要求,我们只有一次买卖股票的机会,设dp[i]表示第i天结束后的最大利润,要想使某日的利润最大,那就需要找出该日之前的价格最小的一天,然后相减,也就是dp[i]=max(dp[i],prices[i]-prices[之前最小的]),这里可以直接省略dp数组,动态维护一个int值。 

1.3.代码实现(1种) 

    class Solution {
        /**
         * dp:dp[i]表示在第i天卖出时的最大利润,此题可以省去dp数组,只需要动态维护一个int值即可
         * 在某一天买入,然后在后边的某一天卖出,当这天卖出时,最大的利润就是当天的价钱-之前的最小价钱
         * 所以需要动态维护i-1天的最小价钱,然后计算第i天卖出的价钱,再去动态维护0~i天的最大利润
         */
        public int maxProfit(int[] prices) {
            int n = prices.length;
            int minPrice = prices[0]; //记录i-1天的最低价钱,初始化为第一天的price
            int maxProfit = 0; //记录最大利润,0表示未买入
            for (int i = 1; i < n; i++) {
                minPrice = Math.min(minPrice, prices[i - 1]);
                maxProfit = Math.max(maxProfit, prices[i] - minPrice);
            }
            return maxProfit;
        }
    }

2.买卖股票的最佳时机II(122题)

2.1.题目

【leetcode买卖股票系列问题】多次买卖/手续费/冻结期_第2张图片

 2.2.思路

与I相比,该题不限制我们买卖股票的次数。

1.贪心:把股票价格看成一个折线图,x轴是天数,y轴是价格,那么折线图上升的时期就是股票盈利的时期,既然不要求买卖次数,那么我们可以两天两天地去判断(因为买入卖出要分为两天),若当天的价格>前一天的价格,那么就把他们的差值累加,最后得到的就是所有上升时期所盈利的利润。

2.dp:设dp[i]为第i天结束后的最大收益,此时分为两种状态,手里有股票dp[i][0]和手里没股票dp[i][1],推动态转移方程:dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i]),可以是上一天就有的股票或者是上一天没有股票,今天才买股票。dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i]),今天没股票,说明昨天也没有股票或者是昨天还有股票,今天卖掉了股票。

2.3.代码实现(3种)

2.3.1 贪心

    public int maxProfit1(int[] prices) {
            int ans = 0;
            for (int i = 1; i < prices.length; i++) {
                if (prices[i] - prices[i - 1] > 0) {
                    ans += prices[i] - prices[i - 1];
                }
            }
            return ans;
        }

 2.3.2 dp(无空间优化)

    public int maxProfit2(int[] prices) {
            int n = prices.length;
            int[][] dp = new int[n][2];
            dp[0][0] = -prices[0]; //第0天有股票说明今天买了,初始化-prices[0],其他值默认为0
            for (int i = 1; i < n; i++) {
                dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
                dp[i][1] = Math.max(dp[i - 1][0] + prices[i], dp[i - 1][1]);
            }
            //最后一天手里还有股票无意义,直接返回dp[n-1][1]
            return dp[n - 1][1];
            //注意到第i天结束后的最大利润之和i-1有关,则可以优化空间复杂度
        }

2.3.3 dp(有空间优化)

        /**
         * 1.贪心:因为不限制买卖次数,只限制同一时间持有一支股票的话,那可以随时进行买卖
         * 只需要计算出所有上升的差值即可,即prices[i]-prices[i-1]
         * 2.dp:设dp[i]代表第i天结束的最大利润,此时有2种状态,手里有股票和没股票
         * 有股票dp[i][0],没有股票是dp[i][1],
         * dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i]),可以是上一天的股票,也可以是上一天没股票,今天才买
         * dp[i][1]=max(dp[i-1][0]+prices[i],dp[i-1][1]),今天没有股票,说明是昨天还有股票,今天卖了,或者是之前就卖了
         */
        public int maxProfit(int[] prices) {
            int n = prices.length;
            int hold = -prices[0]; //第0天有股票说明今天买了,初始化-prices[0]
            int no = 0; //记录当天结束后没有股票
            for (int i = 1; i < n; i++) {
                hold = Math.max(hold, no - prices[i]);
                no = Math.max(hold + prices[i], no);
            }
            //最后一天手里还有股票无意义,直接返回no
            return no;
        }

3.买卖股票的最佳时机III(123题)

3.1.题目

【leetcode买卖股票系列问题】多次买卖/手续费/冻结期_第3张图片

3.2.思路

与II不同的是,本题限制了买卖的次数为2次。

dp:设dp[i]为第i天结束后的最大盈利,那么此时有5种状态,买入第一次股票(dp[i][0])、卖出第一次股票(dp[i][1])、 买入第二次股票(dp[i][2])、 卖出第二次股票(dp[i][3])、什么也不做(此时收益为0,后续不再考虑此类情况)。

推导动态转移方程: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]),表示之前已经卖了第二次或者今天才卖第二次(前提是之前买过第二次)。

3.3.代码实现(2种) 

3.3.1 dp(无空间优化)

    public int maxProfit1(int[] prices) {
            int n = prices.length;
            int[][] dp = new int[n][4];
            dp[0][0] = -prices[0]; //初始化,第0天买入第一次
            dp[0][2] = -prices[0]; //初始化,其中包含了第0天买入第一次和第0天卖出第一次,然后再买入第二次
            for (int i = 1; i < n; 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]);
                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]);
            }
            //最终范围在0 dp[n-1][1] dp[n-1][3]之间,但后者的边界情况已经包含了0,且dp[n-1][3]又包括了同一天买入卖出的情况,所以直接返回即可
            return dp[n - 1][3];
            //浅浅优化一下空间复杂度
        }

3.3.2  dp(有空间优化)

        /**
         * dp:设dp[i]为第i天结束后的最大收益,此时可能有5种状态
         * 0.买了第一次股票 1.卖了第一次股票 2.买了第二次股票 3.卖了第二次股票 4.什么都没干
         * 什么都没干的话收益为0,可以不进行记录
         * 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]),卖了第二次股票,可能是之前卖的,也可能是今天卖的
         */
        public int maxProfit(int[] prices) {
            int n = prices.length;
            int buy1 = -prices[0]; //别忘了初始化
            int sell1 = 0;
            int buy2 = -prices[0]; //别忘了初始化
            int sell2 = 0;
            for (int i = 1; i < n; i++) {
                buy1 = Math.max(buy1, -prices[i]);
                sell1 = Math.max(sell1, buy1 + prices[i]);
                buy2 = Math.max(buy2, sell1 - prices[i]);
                sell2 = Math.max(sell2, buy2 + prices[i]);
            }
            return sell2;
        }

4.买卖股票的最佳时机IV(188题)

4.1.题目

【leetcode买卖股票系列问题】多次买卖/手续费/冻结期_第4张图片

4.2.思路

        与III不同的是,III要求最多两次买卖,本题要求k次,在III种,我们通过构建buy1、sell1、buy2、sell2来动态维护最大利润,III就是IV的一种特殊情况。我们只需要在建立dp数组时把dp数组变成动态的即可。

        在本题中,我们可以看成k次买/卖,设dp[k][0/1]代表第k次的买/卖,这里的dp中存储的是动态维护的最大利润,而不是某天的最大利润,如果要存储每天的最大利润,我们所建立的dp数组就变成了dp[n][k][0/1],代表第n天的第k次买/卖。

        需要注意的是,本题k的范围不固定,当k>数组长度的一半时,dp退化为贪心,有助于提高效率。

 4.3.代码实现(1种)

    class Solution {
        /**
         * dp:设dp[i]表示第i天结束后的最大利润
         * IV和III相似的一点就是,III中规定k=2,也就是有两次的买卖机会,当时我们用buy1,sell1,buy2,sell2表示,
         * 那么IV中也一样,在创建dp的时候设置成动态的就行,
         * 还有一点就是当k>数组长度的一半时,dp退化为贪心,此时类似于II的解法
         */
        public int maxProfit(int k, int[] prices) {
            if (k == 0) { //注意题目所给边界,k可能为0
                return 0;
            }
            if (k > prices.length / 2) { //调用贪心算法
                return greedy(prices);
            }
            //dp
            int[][] dp = new int[k][2]; //一共有k组买卖,dp[i][0]代表第i次买,dp[i][1]代表第i次卖
            for (int i = 0; i < k; i++) {
                dp[i][0] = -prices[0]; //别忘了初始化买入的时候
            }
            for (int i = 1; i < prices.length; i++) {
                //dp[0][0]表示第0次买,因为下一天的买卖之和前一天有关,所以这里节省了空间,否则是dp[n][k][0/1]
                dp[0][0] = Math.max(dp[0][0], -prices[i]); //buy1
                dp[0][1] = Math.max(dp[0][1], dp[0][0] + prices[i]); //sell1
                for (int j = 1; j < k; j++) {
                    //这次的buy是通过上一次的sell-prices[i]得来的
                    dp[j][0] = Math.max(dp[j][0], dp[j - 1][1] - prices[i]); //buy(k+1)
                    dp[j][1] = Math.max(dp[j][1], dp[j][0] + prices[i]); //sell(k+1)
                }
            }
            //和之前一样,后边的卖是包含了前面的卖的,直接返回最后一次卖出所获得的最大利润就可
            return dp[k - 1][1];
        }

        public int greedy(int[] prices) {
            //和II中一样
            int ans = 0;
            for (int i = 1; i < prices.length; i++) {
                if (prices[i] > prices[i - 1]) {
                    ans += prices[i] - prices[i - 1];
                }
            }
            return ans;
        }
    }

5.买卖股票的最佳时机含冷冻期(309题)

5.1.题目

【leetcode买卖股票系列问题】多次买卖/手续费/冻结期_第5张图片

5.2.思路

        与前面都不同的是,本题含有冷冻期。

        dp:设dp[i]为第i天结束后的最大收益,此时可能的状态有,持有股票(dp[i][0]),没有股票且处于冷冻期(dp[i][1]),没有股票且不处于冷冻期(dp[i][2])。(这里的冷冻期是指今天结束后,明天会进入冷冻)。

推导动态转移方程 :

dp[i][0]=max(dp[i-1][0],dp[i-1][2]-prices[i]),表示可能之前就有股票或者之前没股票,但想要今天买股票的话,昨天必须不处于冷冻期;

dp[i][1]=dp[i-1][0]+prices[i],今天进入了冷冻期说明昨天有股票且把股票卖了;

dp[i][2]=max(dp[i-1][2],dp[i-1][1]),今天不持有股票且不处于冷冻期,说明昨天不持有股票处于冷冻期或者不持有股票不处于冷冻期。

5.3.代码实现(2种)

5.3.1 dp(无空间优化)

    public int maxProfit(int[] prices) {
            int n = prices.length;
            int[][] dp = new int[n][3];
            dp[0][0] = -prices[0]; //第0天买入后 dp[0][1]、dp[0][2]默认为0,此时无收益
            for (int i = 1; i < n; i++) {
                dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][2] - prices[i]);
                dp[i][1] = dp[i - 1][0] + prices[i];
                dp[i][2] = Math.max(dp[i - 1][1], dp[i - 1][2]);
            }
            //第n-1天还持有股票的话无意义,直接省略
            return Math.max(dp[n - 1][1], dp[n - 1][2]);
            //注意到dp[i][..]只和dp[i-1][..]有关,可以空间优化,用变量存储dp[i-1][..],空间复杂度O(3n)->O(1)
        }

 5.3.2. dp(有空间优化)

        /**
         * dp:设dp[i][1/2/3]代表第i天结束之后的累计最大收益(此收益可能是负数)
         * 只可能有三种状态:1.持有一支股票 2.不持有股票且处于冷冻期 3.不持有股票且不处于冷冻期
         * 分别对应dp[i][0]、dp[i][1]、dp[i][2],这里的冷冻期是指第i天结束后的状态,也就是i+1天时会被冻结
         * dp[i][0]=max(dp[i-1][0],dp[i-1][2]-prices[i]),可以是上一天的股票,也可以是上上一天卖掉股票后今天又买回来
         * dp[i][1]=dp[i-1][0]+prices[i],明天会被冻结说明今天卖出了股票,那么在i-1天我们就需要持有股票
         * dp[i][2]=max(dp[i-1][1],dp[i-1][2]),不处于冷冻期,说明昨天什么都没干,昨天可能处于冷冻期dp[i-1][1],也可能没处于冷冻期dp[i-1][2]
         */
        public int maxProfit1(int[] prices) {
            int n = prices.length;
            int flag1 = -prices[0]; //别忘了初始化
            int flag2 = 0;
            int flag3 = 0;
            for (int i = 1; i < n; i++) {
                flag1 = Math.max(flag1, flag3 - prices[i]);
                flag2 = flag1 + prices[i];
                flag3 = Math.max(flag1, flag2);
            }
            //第n-1天还持有股票的话无意义,直接省略
            return Math.max(flag2, flag3);
        }

6.买卖股票的最佳时机含手续费(714题)

6.1.题目

【leetcode买卖股票系列问题】多次买卖/手续费/冻结期_第6张图片

6.2.思路

        之前想到用贪心,但是想的太简单了,如果判断一小段时间内是盈利就卖出的话,如果下一天还是盈利,那么就浪费了手续费。 

        dp:设dp[i]为第i天结束后的最大收益,且设手续费是在卖出股票后缴纳。那么此时有两种状态,dp[i][0]表示有股票,dp[i][1]表示无股票。

推导动态转移方程:

dp[i][0]=max(dp[i-1][0],dp[i-1][1]-prices[i]),表示以前就有股票或者是今天才买股票;

dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i]-fee),表示以前就没股票或者今天才卖掉股票(要缴纳手续费)。

6.3.代码实现(2种) 

6.3.1 dp(无空间优化)

    public int maxProfit1(int[] prices, int fee) {
            int n = prices.length;
            int[][] dp = new int[n][2];
            dp[0][1] = -prices[0]; //别忘了初始化
            for (int i = 1; i < n; i++) {
                dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);
                dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
            }
            return dp[n - 1][0];
        }

6.3.2 dp(有空间优化)

        /**
         * dp:和II类似,本题增加了手续费,设dp[i]表示第i天结束后的最大收益,此时有两种状态,
         * 0.无股票 1.有股票 (设手续费是在卖出股票时结算)
         * dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i]-fee),上一天也没股票,或上一天有股票,今天卖出
         * dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i]),上一天的股票,或上一天没股票,今天买的股票
         */
        public int maxProfit(int[] prices, int fee) {
            int n = prices.length;
            int flag1 = -prices[0]; //有股票
            int flag2 = 0; //没股票
            for (int i = 1; i < n; i++) {
                flag1 = Math.max(flag1, flag2 - prices[i]);
                flag2 = Math.max(flag2, flag1 + prices[i] - fee);
            }
            return flag2;
        }

7.股票的最大利润(剑指Offer63题)

        本题与 1.买卖股票的最佳时机(121题)一样,唯一不同的就是数组的大小可能为0,需要判断一下边界。

三、总结

1.dp[i]表示的是第i天结束后的最大收益。

2.要考虑完整的状态,总的状态分为持有股票、不持有股票、什么也不做0收益(不考虑此类),不持有股票的时候可能处于冻结期或者非冻结期。

3.还有手续费的问题,手续费的缴纳时机分为买入和卖出,dp的时候放在买入,贪心的时候放在卖出(没搞懂)。

4.在更新dp数组的时候,已经把当天买入当天卖出的情况融入进去了,对结果不影响。

5.掌握好允许买卖k次的那道题目,其他题目差别不大。

感谢观看,如有问题,欢迎补充! 

你可能感兴趣的:(leetcode,算法,职场和发展)