相关推荐recommended
算法沉淀——动态规划之简单多状态 dp 问题(上)(leetcode真题剖析)
作者:mmseoamin日期:2024-03-04

算法沉淀——动态规划之简单多状态 dp 问题(上)(leetcode真题剖析),在这里插入图片描述,第1张

算法沉淀——动态规划之简单多状态 dp 问题上

  • 01.按摩师
  • 02.打家劫舍 II
  • 03.删除并获得点数
  • 04.粉刷房子

    01.按摩师

    题目链接:https://leetcode.cn/problems/the-masseuse-lcci/

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

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

    示例 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。

    思路

    1. 状态表达: 我们定义两个状态数组,f 和 g:

      • 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])。
          • 初始化: 由于这道题的初始化比较简单,无需加辅助节点,仅需初始化 f[0] = nums[0], g[0] = 0 即可。

          • 填表顺序: 根据状态转移方程,从左往右,两个表一起填。

          • 返回值: 根据状态表达,我们应该返回 max(f[n - 1], g[n - 1])。

    代码

    class Solution {
    public:
        int massage(vector& nums) {
            int n = nums.size();
            if(n==0) return 0;
            vector f(n);
            vector g(n);
            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(g[n - 1], f[n - 1]);
        }
    };
    

    02.打家劫舍 II

    题目链接:https://leetcode.cn/problems/house-robber-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

      思路

      将环形的打家劫舍问题转化为两个单排的问题。具体来说,你分别考虑两种情况:

      a. 偷第一个房屋的情况: 在这种情况下,由于首尾相连,你不能偷最后一个房子,因此偷窃范围是 [0, n - 2]。你可以使用之前解决「打家劫舍I」的动态规划方法来找到在这个范围内的最大金额,得到的结果是 x。

      b. 不偷第一个房屋的情况: 在这种情况下,你可以偷最后一个房子,因此偷窃范围是 [1, n - 1]。同样,使用相同的动态规划方法得到在这个范围内的最大金额,得到的结果是 y。

      最终的答案就是这两种情况下的最大值,即 max(x, y)。

      代码

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

      03.删除并获得点数

      题目链接:https://leetcode.cn/problems/delete-and-earn/

      给你一个整数数组 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 * 104
      • 1 <= nums[i] <= 104

        思路

        其实这道题可以看作是「打家劫舍I」问题的变体。通过将每个数字的出现的和记录在 hash 数组中,然后在 hash 数组上应用「打家劫舍」的思路,你能够有效地解决这个问题。

        具体来说,可以创建一个大小为 10001(根据题目的数据范围)的 hash 数组,将 nums 数组中的每个元素 x 累加到 hash 数组下标为 x 的位置上。然后就可以使用「打家劫舍I」问题的动态规划方法,从 hash 数组中找到不相邻的元素的最大和。

        代码

        class Solution {
        public:
            int deleteAndEarn(vector& nums) {
                int hash[10001] = {0};
                for(int& x:nums) hash[x]+=x;
                vector f(10001);
                vector g(10001);
                for(int i=1;i<10001;++i){
                    f[i]=g[i-1]+hash[i];
                    g[i]=max(g[i-1],f[i-1]);
                }
                return max(f[10000],g[10000]);
            }
        };
        

        04.粉刷房子

        题目链接:https://leetcode.cn/problems/JEj789/

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

          思路

          1. 状态表表示:

            • 在处理线性动态规划时,采用“经验+题目要求”方式定义状态表,选择以某个位置为结尾的方式。
            • 在该位置结束时,定义三种颜色选择的状态表,分别表示最后一个位置选择“红色”、“蓝色”和“绿色”的最小花费。
            • 状态转移方程:

              • 分析三个状态的转移方程,以 dp[i][0] 为例:

                • 若选择在位置 i 粉刷“红色”,考虑前一个位置“蓝色”和“绿色”两种情况的最小花费,再加上当前位置的花费。
                • 类似地,对于 dp[i][1] 和 dp[i][2],分别考虑选择“蓝色”和“绿色”时的最小花费。

                  于是状态方程为:

                  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,确保后续填表的正确性。
                  • 注意辅助节点的值要符合题目的要求。
                  • 填表顺序:

                    • 根据状态转移方程,从左往右同时填充三个表格。
                    • 返回值:

                      • 返回最后一个位置三种颜色选择的最小值,即 min(dp[n][0], min(dp[n][1], dp[n][2]))。

          代码

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