首页 > 编程语言 >Offer必备算法15_简单多问题dp_八道力扣题(打家劫舍+买卖股票)

Offer必备算法15_简单多问题dp_八道力扣题(打家劫舍+买卖股票)

时间:2024-03-23 21:29:25浏览次数:19  
标签:状态 15 Offer int nums 力扣题 vector prices dp

目录

①力扣LCR 089. 打家劫舍

解析代码

②力扣213. 打家劫舍 II

解析代码

③力扣740. 删除并获得点数

解析代码

④力扣LCR 091. 粉刷房子

解析代码

⑤力扣309. 买卖股票的最佳时机含冷冻期

状态机分析

解析代码

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

状态机分析

解析代码

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

状态机分析

解析代码

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

状态机分析

解析代码

本篇完。


①力扣LCR 089. 打家劫舍

三道一样的题:

LCR 089. 打家劫舍

198. 打家劫舍

面试题 17.16. 按摩师

难度 中等

一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响小偷偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组 nums ,请计算 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

输入:nums = [1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

输入:nums = [2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 400
class Solution {
public:
    int massage(vector<int>& nums) {

    }
};

解析代码

以某个位置为结尾,结合题目要求,dp[i] 表示:选择到 i 位置时,此时的最大金额。

但是我们这个题在 i 位置的时候,会面临选择或者不选择两种抉择,所依赖的状态需要细分:

  • f[i] 表示:选择到 i 位置时, nums[i] 必选,此时的最大金额
  • g[i] 表示:选择到 i 位置时, nums[i] 不选,此时的最大金额

因为状态表示定义了两个,因此状态转移方程也要分析两个:

  • 对于 f[i] : 如果 nums[i] 必选,那么仅需知道 i - 1 位置在不选的情况下的最大金额, 然后加上 nums[i] 即可,因此 f[i] = g[i - 1] + nums[i] 。
  • 对于 g[i] : 如果 nums[i] 不选,那么 i - 1 位置上选或者不选都可以。因此,我们需要知道 i - 1 位置上选或者不选两种情况下的最大金额,因此 g[i] = max(f[i - 1], g[i - 1]) 。
class Solution {
public:
    int massage(vector<int>& nums) {
        int n = nums.size();
        if (n == 0)
            return 0; // 处理边界条件
            
        vector<int> f(n);
        auto g = f;
        f[0] = nums[0];
        for (int i = 1; i < n; i++)
        {
            f[i] = g[i - 1] + nums[i];
            g[i] = max(f[i - 1], g[i - 1]);
        }
        return max(f[n - 1], g[n - 1]);
    }
};

②力扣213. 打家劫舍 II

213. 打家劫舍 II

难度 中等

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

示例 1:

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 3:

输入:nums = [1,2,3]
输出:3

提示:

  • 1 <= nums.length <= 100
  • 0 <= nums[i] <= 1000
class Solution {
public:
    int rob(vector<int>& nums) {

    }
};

解析代码

        这一个问题是打家劫舍1问题的变形。 打家劫舍1是一个单排的模式,这一个问题是一个环形的模式,也就是首尾是相连的。但是可以将环形问题转化为两个单排问题:

  • 偷第一个房屋的最大金额 x ,此时不能偷最后一个房子,因此就是偷 [0, n - 2] 区间的房子
  • 不偷第一个房屋的最大金额 y ,此时可以偷最后一个房子,因此就是偷 [1, n - 1] 区间的房子

两种情况下的最大值,就是最终的结果。 因此,问题就转化成求两次单排结果的最大值

class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        return max(nums[0] + rob1(nums, 2, n - 2), rob1(nums, 1, n - 1));
    }

    int rob1(vector<int>& nums, int left, int right)
    {   
        if(left > right)
            return 0;

        int n = nums.size();
        vector<int> f(n);
        auto g = f;
        f[left] = nums[left]; // 初始化
        for(int i = left + 1; i <= right; ++i) // 填表
        {
            f[i] = g[i - 1] + nums[i];
            g[i] = max(f[i - 1], g[i - 1]);
        }
        return max(f[right], g[right]);
    }
};

③力扣740. 删除并获得点数

740. 删除并获得点数

难度 中等

给你一个整数数组 nums ,你可以对它进行一些操作。

每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除 所有 等于 nums[i] - 1 和 nums[i] + 1 的元素。

开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。

示例 1:

输入:nums = [3,4,2]
输出:6
解释:
删除 4 获得 4 个点数,因此 3 也被删除。
之后,删除 2 获得 2 个点数。总共获得 6 个点数。

示例 2:

输入:nums = [2,2,3,3,3,4]
输出:9
解释:
删除 3 获得 3 个点数,接着要删除两个 2 和 4 。
之后,再次删除 3 获得 3 个点数,再次删除 3 获得 3 个点数。
总共获得 9 个点数。

提示:

  • 1 <= nums.length <= 2 * 10^4
  • 1 <= nums[i] <= 10^4
class Solution {
public:
    int deleteAndEarn(vector<int>& nums) {

    }
};

解析代码

        这道题依旧是打家劫舍1问题的变型。 注意到题目描述,选择 x 数字的时候, x - 1 与 x + 1 是不能被选择的。就像打家劫舍问题中,选择 i 位置的金额之后,就不能选择 i - 1 位置以及 i + 1 位置的金额。

        因此,可以创建一个大小为 10001 的 hash 数组(根据题目的数据范围,数据范围不知道可以找nums数组中的最大值),将 nums 数组中每⼀个元素 x ,累加到 hash 数组下标为 x 的位置处,然后在 hash 数组上来一次打家劫舍即可。

class Solution {
public:
    int deleteAndEarn(vector<int>& nums) {
        int N = 1e4 + 1; // 数据范围不知道可以找nums的最大值
        vector<int> hash(N);
        for(auto& e : nums)
        {
            hash[e] += e;
        }
        vector<int> f(N); // 在hash数组做一次打家劫舍dp
        vector<int> g(N);
        for(int i = 1; i < N; ++i)
        {
            f[i] = hash[i] + g[i - 1];
            g[i] = max(f[i - 1], g[i - 1]);
        }
        return max(f[N - 1], g[N - 1]);
    }
};

④力扣LCR 091. 粉刷房子

LCR 091. 粉刷房子

难度 中等

假如有一排房子,共 n 个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。

当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个 n x 3 的正整数矩阵 costs 来表示的。

例如,costs[0][0] 表示第 0 号房子粉刷成红色的成本花费;costs[1][2] 表示第 1 号房子粉刷成绿色的花费,以此类推。

请计算出粉刷完所有房子最少的花费成本。

示例 1:

输入: costs = [[17,2,17],[16,16,5],[14,3,19]]
输出: 10
解释: 将 0 号房子粉刷成蓝色,1 号房子粉刷成绿色,2 号房子粉刷成蓝色
     最少花费: 2 + 5 + 3 = 10。

示例 2:

输入: costs = [[7,6,2]]
输出: 2

提示:

  • costs.length == n
  • costs[i].length == 3
  • 1 <= n <= 100
  • 1 <= costs[i][j] <= 20
class Solution {
public:
    int minCost(vector<vector<int>>& costs) {

    }
};

解析代码

以某个位置为结尾,结合题目要求,定义状态表示:

但在这个题在 i 位置的时候,会面临红蓝绿三种抉择,所依赖的状态需要细分:

  • dp[i][0] 表示:粉刷到 i 位置的时候,最后⼀个位置粉刷上红色,此时的最小花费,
  • dp[i][1] 表示:粉刷到 i 位置的时候,最后⼀个位置粉刷上蓝色,此时的最小花费,
  • dp[i][2] 表示:粉刷到 i 位置的时候,最后⼀个位置粉刷上绿色,此时的最小花费,

因为状态表示定义了三个,因此状态转移方程也要分析三个:

        对于 dp[i][0] :如果第 i 个位置粉刷上红色,那么 i - 1 位置上可以是蓝色或者绿色。因此我们需要知道粉刷到 i - 1 位置上的时候,粉刷上蓝色或者绿色的最小花费,然后加上 i位置的花费即可。于是状态转移方程为:

  • dp[i][0] = min(dp[i - 1][1], dp[i- 1][2]) + costs[i - 1][0] ;

同理,我们可以推导出另外两个状态转移方程为:

  • dp[i][1] = min(dp[i - 1][0], dp[i - 1][2]) + costs[i - 1][1] ;
  • dp[i][2] = min(dp[i - 1][0], dp[i - 1][1]) + costs[i - 1][2] ;

        根据题意和状态转移方程,虚拟节点初始化成0即可,填表顺序是从左往右三个表一起填,最后返回最后一个房子粉刷颜色的最小花费

class Solution {
public:
    int minCost(vector<vector<int>>& costs) {
        int n = costs.size();
        vector<vector<int>> dp(n + 1, vector<int>(3));
        for(int i = 1; i <= n; ++i)
        {   // 注意costs下标映射
            dp[i][0] = min(dp[i-1][1],dp[i-1][2]) + costs[i-1][0];
            dp[i][1] = min(dp[i-1][0],dp[i-1][2]) + costs[i-1][1];
            dp[i][2] = min(dp[i-1][0],dp[i-1][1]) + costs[i-1][2];
        }
        return min(dp[n][0], min(dp[n][1], dp[n][2]));
    }
};

⑤力扣309. 买卖股票的最佳时机含冷冻期

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

难度 中等

给定一个整数数组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
class Solution {
public:
    int maxProfit(vector<int>& prices) {

    }
};

状态机分析

以某个位置为结尾,结合题目要求,定义⼀个状态表示:

由于有买入、可交易、冷冻期三个状态,因此我们可以选择⽤三个数组,其中:

  • dp[i][0] 表示:第 i 天结束后,处于买入状态,此时的最大利润;
  • dp[i][1] 表示:第 i 天结束后,处于可交易状态,此时的最大利润;
  • dp[i][2] 表示:第 i 天结束后,处于冷冻期状态,此时的最大利润。

        处于买入状态的时候,持有股票,此时不能买股票,只能继续持有股票,或者卖出股票。处于可交易状态的时候: 如果在冷冻期,不能买入,如果不在冷冻期,才能买入。


三种状态的转换有个名字叫做状态机:(在下面解释这个图)


对于 dp[i][0] 买入状态,有两种情况能到达这个状态:

  • 从买入到买入:在 i - 1 天持有股票,此时最大收益应该和 i - 1 天的保持一致: dp[i - 1] [0] ;
  • 从可交易到买入:在 i 天买入股票,那我们应该选择 i - 1 天不在冷冻期的时候买入,由于买入需要花钱,所以此时最大收益为: dp[i - 1][1] - prices[i] ;

两种情况应取最大值,因此: dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]) ;


对于 dp[i][1] 可交易状态,有两种情况能到达这个状态:

  • 从可交易到可交易:在 i - 1 天的时候,手上没有股票,也不在冷冻期,但是依旧啥也不⼲到第 i 天,此时 对应的状态为 dp[i - 1][1] ;
  • 从冷冻期到可交易:在 i - 1 天的时候,已经处于冷冻期,然后啥也不⼲到第 i 天,此时对应的状态为: dp[i - 1][2] ;

两种情况应取最大值,因此: dp[i][1] = max(dp[i - 1][1], dp[i - 1][2]) ;


对于 dp[i][2] 冷冻期,只有一种情况能到达这个状态:

在 i - 1 天的时候,卖出股票。 因此对应的状态转移为: dp[i][2] = dp[i - 1][0] + prices[i] ;


        三种状态都会用到前一个位置的值,因此需要初始化每一行的第一个位置:

  • dp[0][0] :此时要想处于买入状态,必须把第⼀天的股票买了,因此 dp[0][0] = - prices[0] ;
  • dp[0][1] :啥也不用干即可,因此 dp[0][1] = 0 ;
  • dp[0][2] :手上没有股票,买⼀下卖⼀下就处于冷冻期,此时收益为 0 ,因此 dp[0][2] = 0 ;

        根据状态表示,三个表从左往右一起填。最后返卖出状态下的最大值,因此应该返回 max(dp[n - 1][1], dp[n - 1] [2]) ;


解析代码

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> dp(n, vector<int>(3));
        dp[0][0] = -prices[0];
        for(int i = 1; i < n; ++i)
        {   // 注意prices下标映射
            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][2]);
            dp[i][2] = dp[i-1][0] + prices[i]; // 在 i-1 天的时候,卖出股票
        }
        return max(dp[n-1][1], dp[n-1][2]);
    }
};

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

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

难度 中等

给定一个整数数组 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 * 10^4
  • 1 <= prices[i] < 5 * 10^4
  • 0 <= fee < 5 * 10^4
class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {

    }
};

状态机分析

以某个位置为结尾,结合题目要求,定义一个状态表示:

由于有买入卖出两个状态,因此可以选择用两个数组,其中:

  • f[i] 表示:第 i 天结束后,处于买入状态,此时的最大利润;
  • g[i] 表示:第 i 天结束后,处于卖出状态,此时的最大利润。

状态机:

状态转移方程:

选择在卖出的时候,支付这个手续费,那么在买⼊的时候,就不用再考虑手续费的问题。

对于 f[i] 买入状态,有两种情况能到达这个状态:

  • 在 i - 1 天持有股票(买入),第 i 天啥也不干。此时最大收益为 f[i - 1] ;
  • 在 i - 1 天的时候没有股票(卖出),在第 i 天买入股票。此时最大收益为 g[i - 1] - prices[i]) ; 

两种情况下应该取最大值,因此 f[i] = max(f[i - 1], g[i - 1] - prices[i]) 。


对于 g[i] 卖出状态,也有两种情况能够到达这个状态:

  • 在 i - 1 天持有股票(买入),在第 i 天将股票卖出,要支付手续费。此时最大收益为: f[i - 1] + prices[i] - fee) ;
  • 在 i - 1 天没有股票(卖出),然后第 i 天啥也不干。此时最大收益为: g[i - 1] ;

两种情况下应该取最大值,因此 g[i] = max(g[i - 1], f[i - 1] + prices[i] - fee) 。


初始化、填表顺序、返回值:

  • 对于 f[0] ,此时处于买入状态,因此 f[0] = -prices[0] ;
  • 对于 g[0] ,此时处于没有股票状态,啥也不干即可获得最大收益,因此 g[0] = 0 ;

填表顺序从左往右两个表一起填,最后返回g [n - 1];(肯定比f[n - 1]大,也可以比较后返回)。


解析代码

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int n = prices.size();
        vector<int> f(n);
        vector<int> g(n);
        f[0] = -prices[0];
        for(int i = 1; i < n; ++i)
        {
            f[i] = max(f[i - 1], g[i - 1] - prices[i]);
            g[i] = max(g[i - 1], f[i - 1] + prices[i] - fee);
        }
        return g[n - 1]; // 肯定比f[n - 1]大
        // return max(g[n - 1], f[n - 1]);
    }
};

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

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

难度 困难

给定一个数组,它的第 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 {
public:
    int maxProfit(vector<int>& prices) {

    }
};

状态机分析

        以某个位置为结尾,结合题目要求,定义一个状态表示: 由于有买入和卖出(可交易)两个状态,因此可以选择用两个数组。但是这道题里面还有交易次数的限制,因此还需要再加上一维,用来表示交易次数。其中:

  • f[i][j] 表示:第 i 天结束后,完成了 j 次交易,处于买入状态,此时的最大利润,
  • g[i][j] 表示:第 i 天结束后,完成了 j 次交易,处于卖出(可交易)状态,此时的最大利润。

状态机:

状态转移方程:

对于 f[i][j] ,有两种情况到这个状态:

  • 在 i - 1 天的时候,交易了 j 次,处于买入状态,第 i 天啥也不干即可。此时最大利润为: f[i - 1][j] ;
  • 在 i - 1 天的时候,交易了 j 次,处于卖出(可交易)状态,第 i 天的时候把股票买了。此时的最大利润为: g[i - 1][j] - prices[i] 。

综上,要的是最大利润,因此是两者的最大值: f[i][j] = max(f[i - 1][j], g[i - 1][j] - prices[i]) ;


对于 g[i][j] ,也有两种情况可以到达这个状态:

  • 在 i - 1 天的时候,交易了 j 次,处于卖出(可交易)状态,第 i 天啥也不干即可。此时的最大利润为: g[i - 1][j] ;
  • 在 i - 1 天的时候,交易了 j - 1 次,处于买入状态,第 i 天把股票卖了,然后就完成了 j 比交易。此时的最大利润为: f[i - 1][j - 1] + prices[i] 。但是这个状态不⼀定存在,要先判断⼀下。

综上,我们要的是最大利润,因此状态转移方程为:

g[i][j] = g[i - 1][j];

if(j - 1 >= 0) g[i][j] = max(g[i][j], f[i - 1][j - 1] + prices[i]);


初始化、填表顺序、返回值:

        对于 f 表由于需要用到 i = 0 时的状态,因此初始化第一行即可。 当处于第 0 天的时候,只能处于买入过一次的状态,此时的收益为 -prices[0] ,因此 f[0][0] = - prices[0] ,其它为负无穷

        对于 g 表由于需要用到 i = 0 时的状态,因此初始化第一行即可。 当处于第 0 天的时候,处于卖出(可交易)状态,啥也不干即可,此时的收益为0 ,因此 f[0][0] = 0,其它为负无穷

        对于负无穷:为了取 max 的时候,⼀些不存在的状态起不到干扰的作用,将它们初始化为负INF (用INT_MIN 在计算过程中会有溢出的风险,这里 INF 折半取 0x3f3f3f3f ,足够小即可)。

填表顺序从左往右两个表一起填,最后返回 g 表最后一行的最大值。


解析代码

class Solution {
    const int INF = 0x3f3f3f3f;
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> f(n, vector<int>(3, -INF)); // 最多两笔
        vector<vector<int>> g(n, vector<int>(3, -INF));
        f[0][0] = -prices[0];
        g[0][0] = 0;
        for(int i = 1; i < n; ++i)
        {
            for(int j = 0; j < 3; ++j)
            {
                f[i][j] = max(f[i-1][j], g[i-1][j] - prices[i]);
                g[i][j] = g[i-1][j];
                if(j-1 >= 0)
                    g[i][j] = max(g[i-1][j], f[i-1][j-1] + prices[i]);
            }
        }
        return max(g[n-1][0], max(g[n-1][1], g[n-1][2]));
    }
};

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

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

难度 困难

给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 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 。

提示:

  • 1 <= k <= 100
  • 1 <= prices.length <= 1000
  • 0 <= prices[i] <= 1000
class Solution {
    const int INF = 0x3f3f3f3f;
public:
    int maxProfit(int k, vector<int>& prices) {

    }
};

状态机分析

        这题和力扣123. 买卖股票的最佳时机 III一样,因为这题用的解题思路是适配这题的,所以把上一题的最多交易两次换成最多交易k次即可,思路基本力扣123一样,看了力扣123可跳过:

        以某个位置为结尾,结合题目要求,定义一个状态表示: 由于有买入和卖出(可交易)两个状态,因此可以选择用两个数组。但是这道题里面还有交易次数的限制,因此还需要再加上一维,用来表示交易次数。其中:

  • f[i][j] 表示:第 i 天结束后,完成了 j 次交易,处于买入状态,此时的最大利润,
  • g[i][j] 表示:第 i 天结束后,完成了 j 次交易,处于卖出(可交易)状态,此时的最大利润。

状态机:

状态转移方程:

对于 f[i][j] ,有两种情况到这个状态:

  • 在 i - 1 天的时候,交易了 j 次,处于买入状态,第 i 天啥也不干即可。此时最大利润为: f[i - 1][j] ;
  • 在 i - 1 天的时候,交易了 j 次,处于卖出(可交易)状态,第 i 天的时候把股票买了。此时的最大利润为: g[i - 1][j] - prices[i] 。

综上,要的是最大利润,因此是两者的最大值: f[i][j] = max(f[i - 1][j], g[i - 1][j] - prices[i]) ;


对于 g[i][j] ,也有两种情况可以到达这个状态:

  • 在 i - 1 天的时候,交易了 j 次,处于卖出(可交易)状态,第 i 天啥也不干即可。此时的最大利润为: g[i - 1][j] ;
  • 在 i - 1 天的时候,交易了 j - 1 次,处于买入状态,第 i 天把股票卖了,然后就完成了 j 比交易。此时的最大利润为: f[i - 1][j - 1] + prices[i] 。但是这个状态不⼀定存在,要先判断⼀下。

综上,我们要的是最大利润,因此状态转移方程为:

g[i][j] = g[i - 1][j];

if(j - 1 >= 0) g[i][j] = max(g[i][j], f[i - 1][j - 1] + prices[i]);


初始化、填表顺序、返回值:

        对于 f 表由于需要用到 i = 0 时的状态,因此初始化第一行即可。 当处于第 0 天的时候,只能处于买入过一次的状态,此时的收益为 -prices[0] ,因此 f[0][0] = - prices[0] ,其它为负无穷

        对于 g 表由于需要用到 i = 0 时的状态,因此初始化第一行即可。 当处于第 0 天的时候,处于卖出(可交易)状态,啥也不干即可,此时的收益为0 ,因此 f[0][0] = 0,其它为负无穷

        对于负无穷:为了取 max 的时候,⼀些不存在的状态起不到干扰的作用,将它们初始化为负INF (用INT_MIN 在计算过程中会有溢出的风险,这里 INF 折半取 0x3f3f3f3f ,足够小即可)。

填表顺序从左往右两个表一起填,最后返回 g 表最后一行的最大值。


解析代码

class Solution {
    const int INF = 0x3f3f3f3f;
public:
    int maxProfit(int k, vector<int>& prices) {
        int n = prices.size();
        k = min(k, n / 2); // 小优化,交易次数不大于天数的一半
        vector<vector<int>> f(n, vector<int>(k+1, -INF));
        vector<vector<int>> g(n, vector<int>(k+1, -INF));
        f[0][0] = -prices[0];
        g[0][0] = 0;
        for(int i = 1; i < n; ++i)
        {
            for(int j = 0; j <= k; ++j)
            {
                f[i][j] = max(f[i-1][j], g[i-1][j] - prices[i]);
                g[i][j] = g[i-1][j];
                if(j-1 >= 0)
                    g[i][j] = max(g[i-1][j], f[i-1][j-1] + prices[i]);
            }
        }
        int ret = 0;
        for(int i = 0; i <= k; ++i)
        {
            ret = max(ret, g[n-1][i]);
        }
        return ret;
    }
};

本篇完。

下一篇是字符串类的OJ。

下下一篇动态规划的类型是子数组子串dp。

标签:状态,15,Offer,int,nums,力扣题,vector,prices,dp
From: https://blog.csdn.net/GRrtx/article/details/136548094

相关文章

  • 设备驱动-15.内核kmalloc/vmalloc及CMA内存介绍
    1kmalloc/vmalloc区别函数位置特性大小限制kmalloc物理内存映射区域物理地址虚拟地址均连续不能超过128Kkzalloc物理内存映射区域物理地址虚拟地址均连续不能超过128Kvmalloc虚拟内存映射区域虚拟地址连续,物理地址不一定连续无限制vzalloc虚拟内......
  • P3592 [POI2015] MYJ
    P3592[POI2015]MYJ要求总和最大,有两张思路:贪心和dp。稍微想一下,发现贪心思考量太大,考虑dp观察n的数据范围,以及转移方式,可以想到区间dp发现转移跟区间最小值有关,设\(f_{l,r,k}\)为区间\([l,r]\)中最小值不小于\(x\)的答案。转移枚举最小值的位置\(p\),\(f_{l,r,k}......
  • CF1615F LEGOndary Grandmaster
    CF1615FLEGOndaryGrandmaster计数好题,转换条件+转化贡献+组合数首先题目的操作没有什么好的性质,考虑一个经典的trick,将奇数位置上的数字取反,于是题目的操作变成\(01\rightarrow10\)或\(10\rightarrow01\)。这个操作的性质就是序列中\(1\)的总数不变,并且操作可以抽象......
  • Day 15(操作符)赋值+单目+关系+逻辑+条件+逗号表达式+下标引用+函数调用
    1.赋值操作符:=   复合赋值符:+=         -=       *=       /=     &=      |=     ^=       %=    >>=    <<=eg: a=a+2→a+=2  a=a>>1→a>>=1连续赋值:a=b=c(从右向左运行)(不推荐此方法)2......
  • CMU15445 2022fall project3
    CMU154452022fallproject3project3相对project2的b+树来说简单太多了,整体没有什么痛苦的debug,基本就看看其他算子的实现参考一下,很快就能写出来。Task1-AccessMethodExecutorsSeqScan首先我们需要知道:init是做一些初始化工作的,next是留给上层节点调用的,SeqScanExecuto......
  • stp的监听和学习状态为什么需要15秒
    STP(生成树协议)的监听和学习状态各自需要15秒,这主要是为了确保网络在角色选举和地址学习的过程中有足够的稳定性和准确性。1.监听状态需要15秒,主要是为了避免STP协议在收敛过程中产生临时环路。监听状态会持续15秒,以确保BPDU(桥接协议数据单元)有足够的时间在整个网络进行传递。......
  • 代码随想录算法训练营第day54|392.判断子序列 、 115.不同的子序列
    目录392.判断子序列115.不同的子序列392.判断子序列力扣题目链接(opensnewwindow)给定字符串s和t,判断s是否为t的子序列。字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而......
  • 15,zabbix-elk
    1、安装logstash2、监控/home/elk/test.log文件[[email protected]]#[[email protected]]#ifconfigeth0:flags=4163<UP,BROADCAST,RUNNING,MULTICAST>mtu8500inet10.206.16.11netmask255.255.240.0broadcast10......
  • Android开发笔记[15]-设置页
    摘要使用MMKV数据框架实现设置页数据同步,设置页可以对其他页面进行设置;设置页数据通过MMKV框架持久化存储,重启APP不丢失.关键信息AndroidStudio:Iguana|2023.2.1Gradle:distributionUrl=https://services.gradle.org/distributions/gradle-8.4-bin.zipjvmTarget='1.......
  • 【LeetCode-153.寻找旋转排序数组的最小值】
    已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums=[0,1,2,4,5,6,7] 在变化后可能得到:若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]注意,数组 [a[0],a[1],a[2],...,a[n-1......