10.动态规划:状态机DP和买卖股票问题【灵神基础精讲】

买卖股票的最佳时机:无限次/冷冻期/k次【基础算法精讲 21】

来自0x3f:https://www.bilibili.com/video/BV1ho4y1W7QK/

介绍了【买卖股票系列问题】与【状态机 DP】,包括【至多/恰好/至少】的讲解。

状态机DP

https://leetcode.cn/problems/maximum-subarray-sum-with-one-deletion/solution/dian-xing-de-zhuang-tai-ji-dpwen-ti-by-y-q6mq/

什么是状态机DP?

不知道状态机是什么的话,放一个简单版的进程五状态模型图就明白了。

在这里插入图片描述
而状态机DP,简单地说,dp[i] 一个值不够用了,需要多几个了。每个值就相当于上图中一个圆圈,各个状态之间可以相互转化。

典型的状态机DP问题有?

  • 股票问题:由于股票存在多次买入买出,也就是dp[i] 存在多种可能(还没卖、还没买、处于冷冻期不能买等),所以想到用状态机DP。力扣搜股票关键字即可,一共有四五题吧。这里附一个 123 买卖股票的最佳时机 Ⅲ

  • 最大连续乘积问题:和最大子数组和相比,乘法会出现负数,此时最小变最大、最大变最小。因此dp[i]应该存最大值和最小值两个状态。 152 乘积最大子数组

  • 摆动序列问题:有时摆动序列以下降结尾,下一个值应该是上升;有时则相反。向前找dp[i]时,最大最小都得用到,所以存两个值。376 摆动序列

状态机DP问题的解法

1. 找到k个状态

股票问题是最典型的状态机DP问题。以此为例我们考虑,遍历到i时,当天卖出、当天不卖、有冷冻期不能卖、无冷冻期不能卖等,什么是状态,需要我们新增一个变量去控制?

  • “当天卖出”不属于状态,属于动作。动作用于连接状态,但不占有dp的状态位。

  • “当天不持有股票”是静态描述,属于状态。

  • “当天处于冷冻期”是静态描述,属于状态。

由此,以动静区分。简单题非常容易看清楚几个状态,但难题很容易混淆。状态没找好,后面就全乱了。

2. 初始化k个变量

初始化为第0个数执行各个动作以后的状态即可。后面从nums[1]遍历。

3. 依据状态之间的关系逐个更新

具体问题具体分析即可。

买卖股票问题

(无限次)122. 买卖股票的最佳时机 II

难度中等2076

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

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

返回 你能获得的 最大 利润

示例 1:

输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
     总利润为 4 + 3 = 7 。

示例 2:

输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
     总利润为 4 。

示例 3:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。

提示:

  • 1 <= prices.length <= 3 * 104
  • 0 <= prices[i] <= 104

题解:prices = [7,1,5,3,6,4]

启发思路:最后一天发生了什么?

从第0天开始到第5天结束时的利润 = 从第0天开始到第4天结束时的利润 + 第五天的利

  • 第五天的利润:①不做 0 ②买入股票-4 ③卖出股票+4

关键词:天数、是否持有股票

**子问题?**到第i天结束时,持有/未持有股票的最大利润

**下一个子问题?**到第i-1天结束时,持有/未持有股票的最大利润
在这里插入图片描述

  • 这种表示状态之间转换关系的图叫状态机,从上图看出状态转移一共有四种情况

定义状态转移方程:
在这里插入图片描述

递归边界:

dfs(-1, 0) = 0 : 第0天开始未持有股票,利润为0

dfs(-1, 1) = -∞:第0天开始不可能持有股票

递归入口max(dfs(n-1, 0), dfs(n-1,1)) = dfs(n-1, 0)

记忆化搜索

class Solution {
    int[] prices;
    int[][] cache;
    public int maxProfit(int[] prices) {
        int n = prices.length;
        this.prices = prices;
        this.cache = new int[n][2];
        for(int i = 0; i < n; i++) Arrays.fill(cache[i], -1);
        return dfs(n-1, 0);
    }

    // hold = 1 持有股票,hold = 0 未持有股票
    public int dfs(int i, int hold){
        if(i < 0) return hold == 1 ? Integer.MIN_VALUE : 0;
        if(cache[i][hold] != -1) return cache[i][hold];
        if(hold == 1){
            // dfs(i, 1) = dfs(i-1, 1) 什么也不做
            // dfs(i, 1) = dfs(i-1, 0) - prices[i] 在i时刻买入股票
            return cache[i][hold] = Math.max(dfs(i-1, 1), dfs(i-1, 0) - prices[i]);
        }
        // dfs(i, 0) = dfs(i-1, 0) 什么也不做
        // dfs(i, 0) = dfs(i-1, 1) + prices[i] 在i时刻卖出股票
        return cache[i][hold] = Math.max(dfs(i-1, 0), dfs(i-1, 1) + prices[i]);
    }
}

翻译成递推:
在这里插入图片描述

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[][] f = new int[n+1][2];
        f[0][1] = Integer.MIN_VALUE; // 0时刻不可能持有股票
        for(int i = 0; i < n; i++){
            f[i+1][0] = Math.max(f[i][0], f[i][1] + prices[i]);
            f[i+1][1] = Math.max(f[i][1], f[i][0] - prices[i]);
        }
        return f[n][0]; // n时刻不持有股票
    }
}

空间优化:由于f[i+1]只用了f[i]这个状态,因此用两个变量滚动计算

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int f0 = 0;
        int f1 = Integer.MIN_VALUE; // 0时刻不可能持有股票
        for(int p : prices){
            int newf0 = Math.max(f0, f1 + p); // 未持有:什么都不做,卖出股票
            f1 = Math.max(f1, f0 - p); // 持有 : 什么都不做,买入股票
            f0 = newf0;
        }
        return f0; // n时刻不持有股票
    }
}

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

难度中等1468

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

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

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

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

示例 1:

输入: prices = [1,2,3,0,2]
输出: 3 
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

示例 2:

输入: prices = [1]
输出: 0

提示:

  • 1 <= prices.length <= 5000
  • 0 <= prices[i] <= 1000

题解:(在买入股票的时候前一天是不能卖出股票的)

含冷冻期问题类似打家劫舍,既然前一天不能有卖出操作,那么直接从i-2,0转移过来

class Solution {
    int[] prices;
    int[][] cache;
    public int maxProfit(int[] prices) {
        int n = prices.length;
        this.prices = prices;
        this.cache = new int[n][2];
        for(int i = 0; i < n; i++) Arrays.fill(cache[i], -1);
        return dfs(n-1, 0);
    }

    // hold = 1 持有股票,hold = 0 未持有股票
    public int dfs(int i, int hold){
        if(i < 0) return hold == 1 ? Integer.MIN_VALUE : 0;
        if(cache[i][hold] != -1) return cache[i][hold];
        if(hold == 1){
            // dfs(i, 1) = dfs(i-1, 1) 什么也不做
            //###########################################################
            // 只修改了在i时刻买入股票,从dfs(i-2, 0) - prices[i])转移过来
            // dfs(i, 1) = dfs(i-2, 0) - prices[i] 在i时刻买入股票
            return cache[i][hold] = Math.max(dfs(i-1, 1), dfs(i-2, 0) - prices[i]);
        }
        // dfs(i, 0) = dfs(i-1, 0) 什么也不做
        // dfs(i, 0) = dfs(i-1, 1) + prices[i] 在i时刻卖出股票
        return cache[i][hold] = Math.max(dfs(i-1, 0), dfs(i-1, 1) + prices[i]);
    }
}

转成递推

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[][] f = new int[n+2][2];
        f[1][1] = Integer.MIN_VALUE; // 0时刻不可能持有股票
        for(int i = 0; i < n; i++){
            f[i+2][0] = Math.max(f[i+1][0], f[i+1][1] + prices[i]);
            f[i+2][1] = Math.max(f[i+1][1], f[i][0] - prices[i]);
        }
        return f[n+1][0]; // n时刻不持有股票
    }
}

(至多K次)188. 买卖股票的最佳时机 IV

难度困难921

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

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

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

示例 1:

输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

示例 2:

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

提示:

  • 0 <= k <= 100
  • 0 <= prices.length <= 1000
  • 0 <= prices[i] <= 1000

题解:因为限制了交易次数,所以要在递归过程中记录交易次数

在这里插入图片描述

递归边界和递归入口:

在这里插入图片描述

记忆化搜索

class Solution {
    int[] prices;
    int[][][] cache;
    public int maxProfit(int k, int[] prices) {
        int n = prices.length;
        this.prices = prices;
        this.cache = new int[n][k+1][2];
        for(int i = 0; i < n; i++)
            for(int j = 0; j <= k; j++)
                Arrays.fill(cache[i][j], -1);
        return dfs(n-1, k, 0);
    }
    // 第i天结束时至多完成j笔交易,持有/未持有股票的最大利润
    // hold = 1 持有股票,hold = 0 未持有股票
    public int dfs(int i, int j, int hold){
        if(j < 0) // 至多交易j次,j<0 不合法的方案
            return Integer.MIN_VALUE;  
        if(i < 0) 
            return hold == 1 ? Integer.MIN_VALUE : 0;

        if(cache[i][j][hold] != -1) return cache[i][j][hold];
        if(hold == 1){
            // dfs(i, j, 1) = dfs(i-1, j, 1) 什么也不做
            // dfs(i, j, 1) = dfs(i-1, j, 0) - prices[i] 在i时刻买入股票
            return cache[i][j][hold] = Math.max(dfs(i-1, j, 1), dfs(i-1, j, 0) - prices[i]);
        }
        // dfs(i, j, 0) = dfs(i-1, j, 0) 什么也不做
        // dfs(i, j, 0) = dfs(i-1, j-1, 1) + prices[i] 在i时刻卖出股票(增加一次交易次数)
        return cache[i][j][hold] = Math.max(dfs(i-1, j, 0), dfs(i-1, j-1, 1) + prices[i]);
    } // 增加交易次数(j-1)写在hold=1和=0都可以

}

转成递推:

为什么有k+2个状态?

这里的第k个状态表示最终结果,正常来说只需要从0k-1个状态即可完成转移,这里总共k个状态,加上需要表示结果的第k个状态,总共k+1个;

需要多加一个-1状态是因为要给这个式子一个惩罚机制,当k小于0是不满足题意的,所以返回负无穷

转成递推之后呢,为了防止数组越界,所以需要将数组第二维度长度整体加一个1,使得-1对上00对上1…,最后返回k+1就是原来的k

在这里插入图片描述

class Solution {
    public int maxProfit(int k, int[] prices) {
        int n = prices.length;
        // 由于需要状态表示i<0 和 j<0 的情况,所以在f和每个f[i]前插入一个状态
        int[][][] f = new int[n + 1][k + 2][2];
        for (int i = 0; i < n; i++)
            for (int j = 0; j <= k + 1; ++j)
                Arrays.fill(f[i][j], Integer.MIN_VALUE / 2);
        for(int j = 1; j <= k+1; j++)
            f[0][j][0] = 0;
        for(int i = 0; i < n; i++){
            for(int j = 1; j <= k+1; j++){
                // hold=0 : cache[i][j][hold] = Math.max(dfs(i-1, j, 0), dfs(i-1, j-1, 1) + prices[i]);
                f[i+1][j][0] = Math.max(f[i][j][0], f[i][j-1][1] + prices[i]);
                // hold=1 : cache[i][j][hold] = Math.max(dfs(i-1, j, 1), dfs(i-1, j, 0) - prices[i]);
                f[i+1][j][1] = Math.max(f[i][j][1], f[i][j][0] - prices[i]);
            }
        }
        return f[n][k+1][0];
    }
}

空间优化:由于f[i+1]只用了f[i]这个状态,去掉一个维度

class Solution {
    public int maxProfit(int k, int[] prices) {
        int n = prices.length;
        // 由于需要状态表示i<0 和 j<0 的情况,所以在f和每个f[i]前插入一个状态
        int[][] f = new int[k + 2][2];
        for (int j = 0; j <= k + 1; ++j)
            f[j][1] = Integer.MIN_VALUE/2;
        f[0][0] = Integer.MIN_VALUE/2;
        for(int p : prices)
            // 和01背包类似,需要倒序遍历背包
            for(int j = k+1; j > 0; j--){
                // hold=0 : cache[i][j][hold] = Math.max(dfs(i-1, j, 0), dfs(i-1, j-1, 1) + prices[i]);
                f[j][0] = Math.max(f[j][0], f[j-1][1] + p);
                // hold=1 : cache[i][j][hold] = Math.max(dfs(i-1, j, 1), dfs(i-1, j, 0) - prices[i]);
                f[j][1] = Math.max(f[j][1], f[j][0] - p);
            }
        return f[k+1][0];
    }
}

恰好K次、至少K次怎么做?

区别在边界上

在这里插入图片描述

1、恰好K次

递归到i<0时,只有j=0是合法的,j>0都是不合法的

# 恰好
class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        # 递推
        n = len(prices)
        f = [[[-inf] * 2 for _ in range(k + 2)] for _ in range(n + 1)]
        f[0][1][0] = 0  # 只需改这里
        for i, p in enumerate(prices):
            for j in range(1, k + 2):
                f[i + 1][j][0] = max(f[i][j][0], f[i][j][1] + p)
                f[i + 1][j][1] = max(f[i][j][1], f[i][j - 1][0] - p)
        return f[-1][-1][0]

        # 记忆化搜索
        # @cache
        # def dfs(i: int, j: int, hold: bool) -> int:
        #     if j < 0:
        #         return -inf
        #     if i < 0:
        #         return -inf if hold or j > 0 else 0
        #     if hold:
        #         return max(dfs(i - 1, j, True), dfs(i - 1, j - 1, False) - prices[i])
        #     return max(dfs(i - 1, j, False), dfs(i - 1, j, True) + prices[i])
        # return dfs(n - 1, k, False)

2、至少K次怎么做?

递归到 至少0次 时,等价于 交易次数没有限制

# 至少
class Solution:
    def maxProfit(self, k: int, prices: List[int]) -> int:
        # 递推
        n = len(prices)
        f = [[[-inf] * 2 for _ in range(k + 1)] for _ in range(n + 1)]
        f[0][0][0] = 0
        for i, p in enumerate(prices):
            f[i + 1][0][0] = max(f[i][0][0], f[i][0][1] + p)
            f[i + 1][0][1] = max(f[i][0][1], f[i][0][0] - p)  # 无限次
            for j in range(1, k + 1):
                f[i + 1][j][0] = max(f[i][j][0], f[i][j][1] + p)
                f[i + 1][j][1] = max(f[i][j][1], f[i][j - 1][0] - p)
        return f[-1][-1][0]

        # 记忆化搜索
        # @cache
        # def dfs(i: int, j: int, hold: bool) -> int:
        #     if i < 0:
        #         return -inf if hold or j > 0 else 0
        #     if hold:
        #         return max(dfs(i - 1, j, True), dfs(i - 1, j - 1, False) - prices[i])
        #     return max(dfs(i - 1, j, False), dfs(i - 1, j, True) + prices[i])
        # return dfs(n - 1, k, False)

练习

(只买一次)121. 买卖股票的最佳时机

难度简单2920

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

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

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

示例 1:

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

提示:

  • 1 <= prices.length <= 105
  • 0 <= prices[i] <= 104

一、前缀和解法:

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[] pre = new int[n];//记录在i之前的最低股票价格
        pre[0] = prices[0];
        for(int i = 1; i < n; i++){
            pre[i] = Math.min(pre[i-1], prices[i]);
        }
        int res = 0;
        for(int i = 0; i < n; i++){
            res = Math.max(res, prices[i] - pre[i]);
        }
        return res;
    }
}

二、股票问题通用解法:

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/solution/gen-zhao-ling-shen-xue-suan-fa-dong-gui-z0fo5/

需要用一个变量 j = 0, 1 来对第i天持有的股票数量进行保存

  • 从最后一天往前推,最后一天可能持有股票也可能不持有股票,那么当然是不持有股票赚的多(持有代表只有买没有卖,且题目限定只有一支股票)
  • 回溯+记忆化搜索:
    1. 若第 i 天持有 1 支股票,那么在第 i-1 天的最大收益可能是在这一天买入了股票 (单纯扣钱) (-price[i]) 或是没有任何操作 (dfs(i-1, 1))
    2. 若第 i 天持有 0 支股票,那么在第 i-1 天的最大收益可能是卖掉了股票 (卖掉加钱) (dfs(i-1, 0)+price[i]) 或是没有任何操作 (dfs(i-1,0))
class Solution {
    int[] prices;
    int[][] cache;
    public int maxProfit(int[] prices) {
        int n = prices.length;
        this.prices = prices;
        cache = new int[n][2];
        for(int i = 0; i < n; i++) Arrays.fill(cache[i], -1);
        return dfs(n-1, 0);
    }

    public int dfs(int i, int j){
        if(i < 0) // 初始状态i=0 不持有状态 ,初始化为0
            return j == 1 ? -prices[0] : 0;
        if(cache[i][j] != -1) return cache[i][j];
        if(j == 1){
            // 持有股票 : 不操作, 在i时刻买入股票
            return cache[i][j] = Math.max(dfs(i-1, 1), -prices[i]);
        }
        // 不持有股票: 不操作, 在i时刻卖出股票
        return cache[i][j] = Math.max(dfs(i-1, 0), dfs(i-1, 1) + prices[i]);
    }
}

改成递推

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[][] f = new int[n+1][2];
        f[0][0] = 0;
        f[0][1] = Integer.MIN_VALUE/2;
        for(int i = 0; i < n; i++){
            f[i+1][0] = Math.max(f[i][0], f[i][1] + prices[i]);
            f[i+1][1] = Math.max(f[i][1], -prices[i]);
        }
        return f[n][0];
    }
}

(至多两次,188弱化版)123. 买卖股票的最佳时机 III

难度困难1388

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

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

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

示例 1:

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

示例 2:

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

示例 3:

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

示例 4:

输入:prices = [1]
输出:0

提示:

  • 1 <= prices.length <= 105
  • 0 <= prices[i] <= 105

三维记忆化搜索【超时】

class Solution {
    int[] prices;
    int[][][] cache;
    public int maxProfit(int[] prices) {
        int n = prices.length;
        this.prices = prices;
        cache = new int[n][3][2];
        for(int i = 0; i < n; i++)
            for(int j = 0; j <= 2; j++)
                Arrays.fill(cache[i][j], -1);
        return f(n-1, 2, 0);
    }

    public int f(int i, int j, int hold){
        if(j < 0) return Integer.MIN_VALUE/2;
        if(i < 0){
            return hold == 0 ? 0 : Integer.MIN_VALUE/2;
        }
        if(cache[i][j][hold] != -1) return cache[i][j][hold];
        if(hold == 1){
            return cache[i][j][hold] = Math.max(f(i-1, j, 1), f(i-1, j, 0) - prices[i]);
        }
        return cache[i][j][hold] = Math.max(f(i-1, j, 0), f(i-1, j-1, 1) + prices[i]);
    }
}

转递推

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int[][][] f = new int[n+1][4][2];
        for(int i = 0; i < n; i++){
            for(int j = 0; j <= 2+1; j++){
                Arrays.fill(f[i][j], Integer.MIN_VALUE/2);
            }
        }
        for(int j = 1; j <= 2+1; j++){
            f[0][j][0] = 0;
        }
        for(int i = 0; i < n; i++){
            for(int j = 1; j <= 2+1; j++){
                f[i+1][j][0] = Math.max(f[i][j][0], f[i][j-1][1] + prices[i]);
                f[i+1][j][1] = Math.max(f[i][j][1], f[i][j][0] - prices[i]);
            }
        }
        return f[n][2+1][0];
    }
}

(无限次+手续费)714. 买卖股票的最佳时机含手续费

难度中等886

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

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

返回获得利润的最大值。

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

示例 1:

输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
输出:8
解释:能够达到的最大利润:  
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8

示例 2:

输入:prices = [1,3,7,5,10,3], fee = 3
输出:6

提示:

  • 1 <= prices.length <= 5 * 104
  • 1 <= prices[i] < 5 * 104
  • 0 <= fee < 5 * 104

题解:只需要在买入或者卖出时添加上手续费,其他与无限次交易相同

记忆化搜索

class Solution {
    int[] prices;
    int[][] cache;
    int fee;
    public int maxProfit(int[] prices, int fee) {
        int n = prices.length;
        this.prices = prices;
        this.fee = fee;
        cache = new int[n][2];
        for(int i = 0; i < n; i++) Arrays.fill(cache[i], -1);
        return f(n-1, 0);
    }

    public int f(int i, int hold){
        if(i < 0) 
            return hold == 0 ? 0 : Integer.MIN_VALUE/2;
        if(cache[i][hold] != -1) return cache[i][hold];
        if(hold == 1){
            return cache[i][hold] = Math.max(f(i-1, 1), f(i-1, 0) - prices[i]);
        }
        return cache[i][hold] = Math.max(f(i-1, 0), f(i-1, 1) + prices[i] - fee);
    }
}

转递推

class Solution {
    public int maxProfit(int[] prices, int fee) {
        int n = prices.length;
        int[][] dp = new int[n+1][2];
        for(int i = 0; i <= n; i++) Arrays.fill(dp[i], Integer.MIN_VALUE/2);
        dp[0][0] = 0;
        for(int i = 0; i < n; i++){
            dp[i+1][0] = Math.max(dp[i][0], dp[i][1] + prices[i] - fee);
            dp[i+1][1] = Math.max(dp[i][1], dp[i][0] - prices[i]);
        }
        return dp[n][0];
    }
}

1911. 最大子序列交替和

难度中等46

一个下标从 0 开始的数组的 交替和 定义为 偶数 下标处元素之 减去 奇数 下标处元素之

  • 比方说,数组 [4,2,5,3] 的交替和为 (4 + 5) - (2 + 3) = 4

给你一个数组 nums ,请你返回 nums 中任意子序列的 最大交替和 (子序列的下标 重新 从 0 开始编号)。

一个数组的 子序列 是从原数组中删除一些元素后(也可能一个也不删除)剩余元素不改变顺序组成的数组。比方说,[2,7,4][4,**2**,3,**7**,2,1,**4**] 的一个子序列(加粗元素),但是 [2,4,2] 不是。

示例 1:

输入:nums = [4,2,5,3]
输出:7
解释:最优子序列为 [4,2,5] ,交替和为 (4 + 5) - 2 = 7 。

示例 2:

输入:nums = [5,6,7,8]
输出:8
解释:最优子序列为 [8] ,交替和为 8 。

示例 3:

输入:nums = [6,2,1,2,4,5]
输出:10
解释:最优子序列为 [6,1,5] ,交替和为 (6 + 5) - 1 = 10 。

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 105

分两种情况讨论:
1.以 i 处结尾时长度为奇数的最大交替和f[i][1]

  • 不用当前元素,为 f[i - 1][1]
  • 可以接在上一个偶数序列后,追加当前元素 f[i - 1][0] + nums[i])

为什么是+nums[i]?,考虑为空序列的情况,当选择当前元素,则从空序列(偶数)转移过来就是加

2.以 i 处结尾时长度为偶数的最大交替和f[i][0]

  • 不用当前元素,为 f[i - 1][0]
  • 可以接在上一个奇数序列后,减去当前元素 f[i - 1][1] - nums[i])

这两种情况,都取各自的最大值,也就是

f[i][0] = Math.max(f[i - 1][0], f[i - 1][1] - nums[i]);
f[i][1] = Math.max(f[i - 1][1], f[i - 1][0] + nums[i]);

由于题中描述 nums[i]>0 所以取奇数列的值一定大于偶数列,最后返回f[n - 1][1]即可

记忆化搜索:

class Solution {
    int[] nums;
    long[][] cache;
    // 交替和 定义为 偶数 下标处元素之 和 减去 奇数 下标处元素之 和
    public long maxAlternatingSum(int[] nums) {
        int n = nums.length;
        this.nums = nums;
        cache = new long[n][2];
        for(int i = 0; i < n; i++) Arrays.fill(cache[i], -1l);
        // 注意到长度为偶数的子序列的最后一个元素在交替和中需要取负号
        // 因此在nums元素均为正数的情况下,不如不计入该元素
        // 因此 f[n][1] > f[n][0] 恒成立
        return f(n-1, 1);
    }

    // f(i, 0): 第 i 个元素处理完后,最优子序列长度为偶数时的最大交替和
    // f(i, 1): 第 i 个元素处理完后,最优子序列长度为奇数时的最大交替和
    // 初始情况 f(0, 0) = 0, f(0, 1) = 负无穷
    public long f(int i, int odd){
        if(i < 0){
            return odd == 1 ? Integer.MIN_VALUE/2 : 0;
        }
        if(cache[i][odd] != -1) return cache[i][odd];
        if(odd == 1){
            // 最优子序列长度为奇数时,
            // 不选i,f(i, 1)从f(i-1, 1)转移过来
            // 选i,  f(i, 1)从f(i-1, 0)+nums[i]转移过来
            return cache[i][odd] = Math.max(f(i-1, 1), f(i-1, 0) + nums[i]);
        }
        return cache[i][odd] = Math.max(f(i-1, 0), f(i-1, 1) - nums[i]);
    }
}

转递推

class Solution {
    public long maxAlternatingSum(int[] nums) {
        int n = nums.length;
        long[][] f = new long[n+1][2];
        f[0][1] = Long.MIN_VALUE/2;
        f[0][0] = 0l;
        for(int i = 0; i < n; i++){
            f[i+1][0] = Math.max(f[i][0], f[i][1] - nums[i]);
            f[i+1][1] = Math.max(f[i][1], f[i][0] + nums[i]);
        }
        return f[n][1];
    }
}

其他题目(笔试题)

恒生 3.31 T2题目(至多+初始资金+股份)

题目来源:https://leetcode.cn/problems/merge-nodes-in-between-zeros/solution/c-by-condescending-carsonsof-5wp3/

在这里插入图片描述

记忆化搜索

class Solution {
    double[] prices;
    double[][][] cache;
    public double maxProfit(int k, double[] prices, double origin) {
        int n = prices.length;
        cache = new double[n][k+1][2];
        for(int i = 0; i < n; i++){
            for(int j = 0; j <= k; j++){
                cache[i][j] = -1;
            }
        }
        // k = 2
        return dfs(n-1, k, 0);
    }

    public double dfs(int i, int j, int hold){
        if(i < 0){
            // 初始资金50000
            return hold == 0 ? origin : Integer.MIN_VALUE/2;
        }
        if(j < 0) return Integer.MAX_VALUE/2;
        if(cache[i][j][hold] != -1) return cache[i][j][hold];
        if(hold == 1){
            return cache[i][j][hold] = Math.max(dfs(i-1, j, 1), dfs(i-1, j-1, 0)/prices[i]);
        }
        return cache[i][j][hold] = Math.max(dfs(i-1, j, 0), dfs(i-1, j, 1)*prices[i]);
    }
}

转递推

class Solution {
    public double maxProfit(int k, double[] prices, double origin) {
        int n = prices.length;
        double[][][] f = new double[n+1][k+2][2];
        for(int i = 0; i <= n; i++){
            for(int j = 0; j <= k+1; j++){
                Arrays.fill(f[i][j], Integer.MIN_VALUE/2); 
            }
        }
        for(int j = 1; j <= k+1; j++){
            f[0][j][0] = origin;
        }
        for(int i = 0; i < n; i++){
            for(int j = 1; j <= k+1; j++){
                f[i+1][j][0] = Math.max(f[i][j][0], f[i][j-1][1] * prices[i]);
                f[i+1][j][1] = Math.max(f[i][j][1], f[i][j][0] / prices[i]);
            }
        }
        return f[n][k+1][0] - origin;
    }
}

空间压缩

class Solution {
    public double maxProfit(int k, double[] prices, double origin) {
        int n = prices.length;
        double[][] f = new double[k+2][2];
        for(int j = 0; j <= k+1; j++){
            Arrays.fill(f[j], Integer.MIN_VALUE/2); 
        }
        for(int j = 1; j <= k+1; j++){
            f[j][0] = origin;
        }
        for(int i = 0; i < n; i++){
            for(int j = k+1; j > 0; j--){
                f[j][0] = Math.max(f[j][0], f[j-1][1] * prices[i]);
                f[j][1] = Math.max(f[j][1], f[j][0] / prices[i]);
            }
        }
        return f[k+1][0] - origin;
    }
}

1262. 可被三整除的最大和

难度中等229

给你一个整数数组 nums,请你找出并返回能被三整除的元素最大和。

示例 1:

输入:nums = [3,6,5,1,8]
输出:18
解释:选出数字 3, 6, 1 和 8,它们的和是 18(可被 3 整除的最大和)。

示例 2:

输入:nums = [4]
输出:0
解释:4 不能被 3 整除,所以无法选出数字,返回 0。

示例 3:

输入:nums = [1,2,3,4,4]
输出:12
解释:选出数字 1, 3, 4 以及 4,它们的和是 12(可被 3 整除的最大和)。

提示:

  • 1 <= nums.length <= 4 * 10^4
  • 1 <= nums[i] <= 10^4

状态机DP

https://leetcode.cn/problems/greatest-sum-divisible-by-three/solution/dong-tai-gui-hua-yu-zhuang-tai-zhuan-yi-by-christm/

1、状态定义

  • dp[i][0]表示nums[0...i]模三余零的最大和
  • dp[i][1]表示nums[0...i]模三余一的最大和
  • dp[i][2]表示nums[0...i]模三余二的最大和
  • 零状态:当前数字最大和模三余零
  • 一状态:当前数字最大和模三余一
  • 二状态:当前数字最大和模三余二

2、状态转移

对于任意一种状态,下一步我们都有两种选择,一是选择当前元素二是不选择当前元素

dp[i][*] = max{dp[i-1][*],dp[i-1][*] + nums[i]}  (* 取值为 0,1,2)

以上是常见的动态规划的递推结构

本题的状态转移显而易见,以当前状态是零状态为例。我们可以想到,前一个状态无非是零状态、一状态、二状态,三种情况,针对这三种情况我们分类讨论即可

class Solution {
    public int maxSumDivThree(int[] nums) {
        int n = nums.length;
        int[][] dp = new int[n+1][3];
        // 初值,不存在余1和余2的情况
        dp[0][0] = 0; dp[0][1] = Integer.MIN_VALUE; dp[0][2] = Integer.MIN_VALUE;
        for(int i = 1; i <= n; i++){
            if(nums[i-1] % 3 == 0){
                dp[i][0] = Math.max(dp[i-1][0], dp[i-1][0] + nums[i-1]);
                dp[i][1] = Math.max(dp[i-1][1], dp[i-1][1] + nums[i-1]);
                dp[i][2] = Math.max(dp[i-1][2], dp[i-1][2] + nums[i-1]);
            }else if(nums[i-1] % 3 == 1){
                dp[i][0] = Math.max(dp[i-1][0], dp[i-1][2] + nums[i-1]);
                dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + nums[i-1]);
                dp[i][2] = Math.max(dp[i-1][2], dp[i-1][1] + nums[i-1]);
            }else{ // nums[i-1] % 3 == 2
                dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + nums[i-1]);
                dp[i][1] = Math.max(dp[i-1][1], dp[i-1][2] + nums[i-1]);
                dp[i][2] = Math.max(dp[i-1][2], dp[i-1][0] + nums[i-1]);
            }
        }
        return dp[n][0]; // 返回模三余0 的值
    }
}

动态规划(更一般性的状态机DP)

https://leetcode.cn/problems/greatest-sum-divisible-by-three/solution/liang-chong-suan-fa-tan-xin-dong-tai-gui-tsll/

讨论:

用【选或不选】的思路,考虑最后一个数 x = nums[n-1]
如果 x mod 3 = 0,那么 x 一定要选,问题变成从 nums[0] 到 nums[n-2] 中寻找能被 3 整除的元素最大和。
如果 x mod 3 = 1:
    如果不选 x,和上面一样,问题变成从 nums[0] 到 nums[n-2] 中寻找能被 3 整除的元素最大和 s0。
    如果选 x,问题变成从 nums[0] 到 nums[n-2] 中寻找最大元素和s2 满足 s2 mod 3 = 2,答案为 max(so,s2 +x)。
如果 x mod 3 = 2:
    如果不选 x,和上面一样,问题变成从 nums[0] 到 nums[n-2] 中寻找能被 3 整除的元素最大和 s0。
    如果选 x,问题变成从 nums[0] 到 nums[n-2] 中寻找最大元素和s1 满足 s1 mod 3 = 1,答案为 max(so,s1 +x)。
---------------------------------------------------
上述讨论,刻画了这道题的两个重要参数
	i: 表示从 nums[0]到 nums[i] 中选数
	j: 表示所选数字之和 s 需要满足 s mod 3 = j。
那么原问题就是 (i = n-1,j = 0),上述讨论得到的子问题有 (i= n-2,j=0),(i = n - 2,j = 1),(i = n - 2,j = 2)。
注: 为什么要从最后一个数开始讨论? 主要是为了方便后面把记忆化搜索改成递推。当然,你从第一个数开始讨论也是可以的。

记忆化搜索

class Solution:
    def maxSumDivThree(self, nums: List[int]) -> int:
        # 定义 dfs(i,j) 表示从 nums[0] 到 nums[i] 中选数, 所选数字之和 s 满足 s mod 3 =j的前提下, s的最大值
        @cache
        def dfs(i, j: int) -> int:
            if i < 0: return -inf if j else 0
            return max(dfs(i-1, j), dfs(i-1, (j + nums[i]) % 3) + nums[i])
        return dfs(len(nums)-1, 0)

转成递推

class Solution {
    public int maxSumDivThree(int[] nums) {
        int n = nums.length;
        int[][] f = new int[n+1][3];
        f[0][0] = 0;
        f[0][1] = Integer.MIN_VALUE;
        f[0][2] = Integer.MIN_VALUE;
        for(int i = 0; i < n; i++){
            for(int j = 0; j < 3; j++)
                f[i+1][j] = Math.max(f[i][j], f[i][(j+nums[i]) % 3] + nums[i]);
        }
        return f[n][0];
    }
}

空间优化:(用滚动数组优化空间)

由于 f[i + 1] 只依赖 f,那么 f[i - 1] 及其之前的数据就没用了

class Solution {
    public int maxSumDivThree(int[] nums) {
        int n = nums.length;
        int[] f = new int[3];
        f[0] = 0;
        f[1] = Integer.MIN_VALUE;
        f[2] = Integer.MIN_VALUE;
        for(int i = 0; i < n; i++){
            int[] tmp = Arrays.copyOf(f, 3);
            for(int j = 0; j < 3; j++)
                f[j] = Math.max(tmp[j], tmp[(j+nums[i]) % 3] + nums[i]);
            tmp = f;
        }
        return f[0];
    }
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值