算法:[动态规划]简单多状态

目录

题目一:按摩师

题目二:打家劫舍II

题目三:删除并获得点数

题目四:粉刷房子

题目五:买卖股票的最佳时机含冷冻期

题目六:买卖股票的最佳时机含手续费


题目一:按摩师

一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。

注意:本题相对原题稍作改动

示例 1:

输入: [1,2,3,1]
输出: 4
解释: 选择 1 号预约和 3 号预约,总时长 = 1 + 3 = 4。

示例 2:

输入: [2,7,9,3,1]
输出: 12
解释: 选择 1 号预约、 3 号预约和 5 号预约,总时长 = 2 + 9 + 1 = 12。

示例 3:

输入: [2,1,4,5,3,1,1,3]
输出: 12
解释: 选择 1 号预约、 3 号预约、 5 号预约和 8 号预约,总时长 = 2 + 4 + 3 + 3 = 12。

按摩师不能连续工作,也就是数组中的数字不能选连续的两个,可以跳过一个,也可以跳过多个,求最大的数字是多少

分为下面五步:

①状态表示

经验 + 题目要求

dp[i] 表示:选择到 i 位置时,此时的最长预约时长

这里由题意可知,又可以细分两个情况,选和不选:

f[i]表示:选择到 i 位置时,nums[i] 必选,此时的最长预约时长

g[i]表示:选择到 i 位置时,nums[i] 不选,此时的最长预约时长

②状态转移方程

此时研究最近的一步,因为上面分了两种情况,所以这里的状态转移方程也分为两种情况:

选择 i 位置时:i - 1位置肯定不选,所以 f[i] = g[i-1] + nums[i]

不选 i 位置时:i - 1位置可选可不选,所以 g[i] = max(f[i-1], g[i-1])

③初始化

初始化为了保证不越界

因为 i 位置需要用到 i - 1 位置的值,所以 i 等于 0 就会越界,所以需要初始化 i == 0的情况

f[0]表示选择0位置时的最大时长,所以 f[0] = nums[0]

g[0]表示不选0位置时的最大时长,所以 g[0] = 0

④填表顺序

因为 g[i] 需要用到 f[i-1] 和 g[i-1],所以:

填表顺序就是从左往右,两个表同时填

⑤返回值

由于分为两种情况,选择和不选,所以返回值就选择 f[n-1]和g[n-1]的最大值

代码如下:

class Solution 
{
public:
    int massage(vector<int>& nums) 
    {
        int n = nums.size();
        // 处理边界情况
        if(n == 0) return 0;
        // 建表
        vector<int> f(n);
        vector<int> g(n);
        // 初始化,g[0]默认为0
        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]);
    }
};

题目二:打家劫舍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又选3

所以这道题只需要分两类情况, 将上一题的思想就直接能够套上去

假设当前区间下标是 0 ~ n - 1

当偷0房屋时,1和n-1就不能偷了,因为是相连的, 只能从2 ~ n - 2 中进行上一题的思想,再加上当前偷的这个房屋的价值即可

当不偷0房屋时,其他房屋不受影响,也就是从 1 ~ n - 1 进行上一题的思想即可 

接着在上面两种情况中,选择较大值作为dp[i]的值

代码如下:

class Solution 
{
public:
    int rob(vector<int>& nums) 
    {
        int n = nums.size();
        // 偷下标为0和不偷下标为0的情况,挑一个最大值
        return max(nums[0] + Use(nums, 2, n - 2), Use(nums, 1, n - 1));
    }

    int Use(vector<int>& nums, int left, int right)
    {
        // 边界情况
        if(left > right) return 0;
        int n = nums.size();
        // 与上一题思想一模一样
        vector<int> f(n);
        vector<int> g(n);
        f[left] = nums[left];
        for(int i = left + 1; i <= right; i++)
        {
            f[i] = nums[i] + g[i - 1];
            g[i] = max(f[i-1], g[i-1]);
        }     
        return max(f[right], g[right]);   
    }
};

题目三:删除并获得点数

给你一个整数数组 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 个点数。

这道题的要求就是选择一个数 x 后,不能选择值为 x - 1 和 x + 1 的数了,如果有一个连续的数,例如 1  2  3  4,选了1,就不能选2,只能选3/4,这就和上面的按摩师的题目是一样的了,都是选择一个数,不能选择与其相连的

但是这道题不同的是,可能并不是连续的数,例如 2  5  5  7,此时选择 2 以后还能选择两个5和7,因为这三个数彼此并没有相差1,那么就需要想办法将这种情况转化为上面的情况

创建一个数组,将每一个数放入下标为这个数的位置,例如 2  5  5  7,也就是将2放入下标为2,两个5加起来是10,10放入下标为5的位置,7放入下标为7的位置

此时判断只需要看数组下标是否相邻即可,也就是不能连续选数组中的两个相邻下标的数

也就转化为了上面的题的做法

代码如下:

class Solution 
{
public:
    int deleteAndEarn(vector<int>& nums) 
    {
        const int n = 10001;
        // 将所有数映射进入arr
        int arr[n] = {0};
        for(auto it : nums) arr[it] += it;
        // 和之前一样,f[i]表示选择i位置,g[i]表示不选i位置
        vector<int> f(n), g(n);
        for(int i = 1; i < n; i++)
        {
            f[i] = arr[i] + g[i - 1];
            g[i] = max(f[i - 1], g[i - 1]);
        }
        return max(f[n - 1], g[n - 1]);
    }
};

题目四:粉刷房子

假如有一排房子,共 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

题意就是每一行表示第几个房子,每一列的0、1、2表示红蓝绿三种颜色的花费

并且相邻的两个房子颜色不能相同

①状态表示

经验 + 题目要求 :以 i 位置为结尾时的最小成本

有三种颜色可选,所以分三种情况

②状态转移方程

所以状态转移方程,当 i 位置涂某一个颜色时,它的前一个位置就选两外两个颜色的最小值 + costs[i - 1][0]

所以三个状态转移方程是:

③初始化

需要初始化dp表的0号下标,我们可以在表前面提前创建一个位置,这样就不需要初始化了,只需要处理这三个位置的值是多少即可

因为填 i 位置时,需要选 i - 1 位置的另外两个颜色的最小值,又因为是填第一个位置的值,所以为了不影响后续结果,将前面0位置全部初始化为0,这样就不会影响结果,在填dp[i][1]时,就只会取它本身的值

最后需要注意下标的映射关系

④填表顺序

从左往右,一次填三个表

⑤返回值

取 dp[n][0] / dp[n][1] / dp[n][2] 的最小值,然后返回

因为dp[n][0]就表示最后一个房子涂上红色,此时的最小花费,以此类推

代码如下:

class Solution 
{
public:
    int minCost(vector<vector<int>>& costs) 
    {
        int n = costs.size();
        // 每种颜色前多创建一个位置,所以创建n+1行
        vector<vector<int>> dp(n + 1, vector<int>(3));
        // 初始化多创建的那一行应该初始化为0
        // 但是创建的vector本身就默认初始化为0,所以不需要处理
        for(int i = 1; i <= n; i++)
        {
            // i位置选红,在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];
        }
        // 选每一个房子的最小值
        return min(min(dp[n][0], dp[n][1]), dp[n][2]);
    }
};

题目五:买卖股票的最佳时机含冷冻期

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

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

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

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

示例 1:

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

示例 2:

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

题目的买卖股票的条件:

(1)、卖出股票后第二天不能买入,有冷冻期
(2)、不能参与多笔交易

①状态表示

因为当天可以分为三种状态:买入(0)、可交易(1)、冷冻期(2)

②状态转移方程

箭头的起点是前一天的状态,箭头指向的时当天的状态:

根据上面的图可以写出状态转移方程

dp[i][0] = max(dp[i-1][0], dp[i-1][1] - price[i])

dp[i][1] = max(dp[i-1][1], dp[i-1][2])

dp[i][2] = dp[i-1][0] + price[i]

③初始化

上述的状态转移方程中,dp[i] 中都用到了 dp[i-1],所以需要初始化0位置

当天结束以后处于买入状态,所以当天买了股票,相当于花掉了当天股票的钱
dp[0][0] = -price[0]

当天结束以后处于可交易状态,所以当天什么都没干
dp[0][1] = 0

当天结束以后处于冷冻期,所以当天买了又卖了,相当于没赚没亏
dp[0][2] = 0

④填表顺序

从左往右填表,一次填写三个表

⑤返回值

返回最后一天结束以后的最大利润

也就是dp[n-1][0]、dp[n-1][1]、dp[n-1][2]的最大利润,因为最后一天如果还是买入状态,没有卖出去股票,那肯定就不是最大利润了,所以可以不计算 dp[n-1][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],其他两个都是0,不需要初始化
        dp[0][0] = -prices[0];
        for(int i = 1; i < n; i++)
        {
            // 根据状态机得到的三个状态转移方程
            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];          
        }
        // 返回最后一天后两种状态的最大值
        return max(dp[n-1][1], dp[n-1][2]);
    }
};

题目六:买卖股票的最佳时机含手续费

给定一个整数数组 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

本题可以无限次的交易,但是每次需要支付手续费,买入卖出只需要交一次手续费

①状态表示

可以选择创建一个 n * 2 的dp表,这里选择创建两个dp表

买入表示持有股票的状态,卖出表示可交易状态

②状态转移方程

同样画出状态机图,得到状态转移方程:

由于是有手续费存在的,所以确定在卖出时交手续费

f[i] = max(f[i-1], g[i-1] - price[i]);

g[i] = max(g[i-1], f[i-1] + price[i] - fee);

③初始化

同样i位置需要用到 i - 1 位置的值,所以需要初始化 0 

f[0] = -price[0]

g[0] = 0

④填表顺序

从左往右,两个表一起填

⑤返回值

因为最后一天如果你手中还有股票没有卖出去,那么肯定不是最大利润,所以只需要返回g[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];
    }
};

下面是创建一个 n * 2 的dp表,其中 dp[i][0] 表示第 i 天的状态是买入,dp[i][1] 表示第 i 天的状态是卖出

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

题目七:买卖股票的最佳时机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


题目八:买卖股票的最佳时机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 。


[动态规划]简单多状态题目到此结束

猜你喜欢

转载自blog.csdn.net/m0_64411530/article/details/140891175