【动态规划算法】专题三——简单多状态dp问题

文章目录

一、面试题 17.16. 按摩师

Leetcode链接

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

示例 :

输入: 2,7,9,3,1

输出: 12

解释: 选择 1 号预约、 3 号预约和 5 号预约,总时长 = 2 + 9 + 1 = 12。

解题思路

  • 对于本题,我们发现按原来的思路"寻找递归子问题"+"使用总结的方法一、方法二"进行状态表示的思考,是很难用单一状态表示去描述出(0,n-1)范围内与(0,n-2)范围内它们之间的关系的,必须要分类讨论(不同限制条件下)才能描述得清楚,像这样的题目就是多状态dp问题,我们就需要使用多个dp状态表示来解题

那么状态表示可以这样定义:

  • 定义两个dp表:fi和gi,fi表示当不选择numsi时,从头开始到i位置这个范围内的最优选择的时长,gi表示当会选择numsi时,从头开始到i位置这个范围内的最优选择的时长

那么经过这样的分类讨论后,就根据相邻状态之间的关系去得出思路了:

  • 我们发现当不选择numsi时,i-1位置可以选也可以不选,那么可得出f[i]=Math.max(f[i-1],g[i-1])
  • 当选择numsi时,i-1位置是不可以再选了的,所以得出g[i]=f[i-1]+nums[i]
  • 这样从前往后填完两个表,最终max( fn-1 , gn-1 ) 就是答案(当然dp表照常进行初始化的判断)

代码实现及解析

java 复制代码
class Solution {
    public int massage(int[] nums) {
        int n=nums.length;
        int[] f=new int[n];
        int[] g=new int[n];
        //状态表示:
        //f[i]表示当不选择nums[i]时,从头开始到i位置这个范围内的最优选择的时长
        //g[i]表示当会选择nums[i]时,从头开始到i位置这个范围内的最优选择的时长

        if(n==0) return 0;//处理空数组情况
        g[0]=nums[0];//初始化dp表
        //填表:
        for(int i=1;i<n;i++){
            f[i]=Math.max(f[i-1],g[i-1]);
            g[i]=f[i-1]+nums[i];
        }
        return Math.max(f[n-1],g[n-1]);
    }
}

总结

  • 复习解题思路

二、打家劫舍 II

Leetcode链接

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

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

【打家劫舍I】就是本题房屋不成圈的版本,那其实就是与上题一模一样的了

解题思路

  • 本题虽然房屋成环,但是偷窃的顺序还是按数组从前往后的,还是不可以到了最后一个房子再走到第一间房子的

通过分类讨论,将环形的问题,转化成线性的【打家劫舍I】:

  • 那么本题也是分类讨论,首先对于成环的特性,只需要对于0、1、n-1这三个位置进行讨论,当0位置选择,那么1、n-1位置都不可以选择了,那不就又成了下标范围为2~(n-2)的【打家劫舍I】问题了嘛
  • 当0位置不选择,那么其他位置都自由选择,那不就又成了下标范围为1~(n-1)的【打家劫舍I】问题了嘛

本题开始可以对多状态dp问题进行一个解题总结:

  • 一个位置有多个状态,而当该位置的状态确定之后,其相邻位置的状态被限制(一个位置的决策会影响到相邻位置的决策,进而这样影响其他所有位置的决策),像这样的问题就是多状态dp问题
  • 我们可以采用:其他多状态dp问题--->【打家劫舍I】问题 ,这样的一个问题转化,这是一个很好的解题方法

三、删除并获得点数

Leetcode链接

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

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

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

解题思路

  • 根据上题所说的解题经验,我们可以察觉到所谓"必须删除所有等于 numsi - 1 和 numsi + 1 的元素"就是对numsi元素相邻元素的限制(数值相邻,还不是位置相邻),也就是多状态dp问题
  • 我们尝试对其进行"其他多状态dp问题--->【打家劫舍I】问题"的转化,我们发现当选择了numsi时,如果想要达到最终的点数最大应该同时将其他的numsi也选择了,不然下次选择其他数值时可能会删除掉本次的numsi,而且既然本题的数据不是位置相邻之间的限制而是数值相邻之间的限制,我们也应该想办法将其数值相邻的元素放到位置相邻。
  • 但是nums中的数据不可能正好使所有数据是连续的(比如,1、2、3、4可以,但是1、3、4就不行),这样就不满足相邻元素之间的限制,所以我们还要将缺少的元素合理地补上
  • 综上,hash表将是一个不错的选择,将元素的值与hash数组的下标对应,将其出现的次数进行累加记录(也可以直接在hashi上累加其点数,这样省了一步),nums中每出现的数再hash中记录就为0,这样hash表最终就对应"房子",hashi就对应房子中的钱,也就完成了到【打家劫舍I】的转化

代码实现及解析

java 复制代码
class Solution {
    public int deleteAndEarn(int[] nums) {
        //1.将相同的点数统计到一块,不同的点数分别放到hash表对应位置:
        int[] hash=new int[10001];//1 <= nums[i] <= 10^4,直接一次性给够空间
        for(int x:nums){
            hash[x]+=x;
        }//最终的hash表就代表房子,hash[i]就代表房子中的钱

        //2.以下对hash数组进行【打家劫舍I】的操作就行了:

        //定义两个dp表:
        int[] f=new int[10001];
        int[] g=new int[10001];
        //dp[i]表示从起点到i位置这个范围内选hash[i](f表)/不选hash[i](g表)可获得的最大点数
        f[0]=hash[0];
        //填表:
        for(int i=1;i<10001;i++){
            f[i]=g[i-1]+hash[i];
            g[i]=Math.max(f[i-1],g[i-1]);
        }
        return Math.max(f[10000],g[10000]);
    }
}

总结

  • 复习解题思路

【股票DP问题】:

四、 买卖股票的最佳时机含冷冻期

Leetcode链接

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

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

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

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

示例 1:

输入: prices = 1,2,3,0,2

输出: 3

解释: 对应的交易状态为: 买入, 卖出, 冷冻期, 买入, 卖出

股票类问题题意的一般性解释:

  • 有个股票,每天价格会变化+其他限制条件,你每天对这个股票进行操作,得出怎样得到最大利润
  • 理论一天内可以对该股票进行购入+卖出的操作,但这没有任何意义,跟不进行交易这个操作不是一样的吗?
  • 购入股票+卖出股票=一轮完整交易

解题思路

  • 按照以前的定义状态表示的经验,我们本题可以进行"从开始到 i 位置为结尾,当i位置的状态为...时,得到的最值是...",本题也差不多,但是股票类题目的状态表示会有些不同:"第 i 天结束后,处于...的状态,此时得到的最值是..."
  • 那么通过分析,本题一个位置也是具有多类状态可表示,那么股票类问题也基本都是这样特殊的状态表示:"第 i 天结束后处于持有股票的状态" or "第 i 天结束后处于没有持有股票的状态但非冷冻期" or "第 i 天结束后处于没有持有股票的状态且处于冷冻期"

接下来分析状态转移方程:

股票问题分析状态转移方程时建议画出各状态之间的关系图:

这样分析出个状态之间的互相转换关系后,就可以根据"都有哪些状态指向了我?"这样的方法去分析状态转移方程:

dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);

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

dp[i][2]=dp[i-1][0]+prices[i];

  • 然后就是根据状态转移方程来做好初始化,有哪些位置会在计算时越界就将哪些位置初始化一下

代码实现及解析

java 复制代码
class Solution {
    public int maxProfit(int[] prices) {
        int n=prices.length;
        int[][] dp=new int[n][3];
        //状态表示(也可以使用三个dp表):
        //dp[i][0]:第i天结束之后状态为"持有股票"时所能获得的最大利润;
        //dp[i][1]:第i天结束之后状态为"手中没有持有股票但非冷冻期"时所能获得的最大利润;
        //dp[i][2]:第i天结束之后状态为"手中没有持有股票且在冷冻期"时所能获得的最大利润

        //初始化:
        dp[0][0]=-prices[0];dp[0][1]=0;dp[0][2]=0;
        //填表:
        for(int i=1;i<n;i++){
            //状态转移方程(将第i天结束时不同状态下的最大利润值依次填上):
            dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);
            dp[i][1]=Math.max(dp[i-1][1],dp[i-1][2]);
            dp[i][2]=dp[i-1][0]+prices[i];
        }
        //选出最后一天结束时,不同状态下利润的最大值:
        return Math.max(dp[n-1][1],dp[n-1][2]);
        //(dp[n-1][0]不参与比较,因为如果都最后一天了手里还留着这个股票,那这肯定不是最佳方案,卖出它利润就已经会变得更高了,但是中间的计算肯定还是要它参与的)
    }
}

总结

  • 复习解题思路和代码注释

五、买卖股票的最佳时机 IV

Leetcode链接

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

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

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

解题思路

  • 状态表示与上题差不多,只是多了一个维度,那就把dp的维度也增加就行了:fij:第i天过后,持有股票且交易次数为 j 时的最大利润;gij:第i天过后,没有持有股票且交易次数为 j 时的最大利润

那么依然画出状态转移图:

得出状态转移方程:

f[i][j]=Math.max(f[i-1][j],g[i-1][j]-prices[i]);

g[i][j]=Math.max(g[i-1][j],f[i-1][j-1]+prices[i]);

  • 那么依然根据状态转移方程对两个dp表进行初始化,我们注意到有个特殊的地方,两个dp表:dp01、dp02、dp03...,第1天过去之后根本不可能完成>=1次交易,所以这些地方是不合法的位置,对与这类位置我们不能乱赋值,要看看其是否会对于后续正常位置的计算造成影响,而本题就会有这个问题,所以为了不对后续计算造成干扰,我们直接将这些位置初始化为负无穷,但本题的dp表中的值还会进行加减运算,如果使用Integer.MAX_VALUE 的话会造成溢出,所以可以使用0x3f3f3f3f这个数,它的无穷大的一半,使用-0x3f3f3f3f既解决了在max()运算中对正常位置计算的干扰问题,因为它足够小,而它在进行运算时也不会出现溢出问题(后续的那些同样不合法的位置全部由这几个位置"传播性"地解决了,因为它们的填表恰好都是依靠前面的不合法位置)
  • 还有fi-1j-1也会造成f表的第一列的越界,但是我们就不对这一列进行初始化了,很麻烦,直接在填表时进行if(j-1>=0)的判断就方便多了,这一列dp表的初始化可以很方便地使用if语句来解是因为本题中填表本来就是依据max()的判断,所以我们不一定非要访问到这个越界的位置,以前填表是只有i-1这一种选择,所以只能选择进对其进行初始化

代码实现及解析`

java 复制代码
class Solution {
    public int maxProfit(int k, int[] prices) {
        int n=prices.length;
        int[][] f=new int[n][k+1];
        int[][] g=new int[n][k+1];
        //dp表的初始化:
        f[0][0]=-prices[0];g[0][0]=0;//两个合法位置进行手动初始化
        int INF=0x3f3f3f3f;//infinity:无穷大
        for(int j=1;j<=k;j++){
            f[0][j]=g[0][j]=-INF;//将这些不合法的位置全部初始化为负无穷大,以防其干扰后续正常位置的计算
        }
        //填表:
        for(int i=1;i<n;i++){
            for(int j=0;j<=k;j++){
                f[i][j]=Math.max(f[i-1][j],g[i-1][j]-prices[i]);
                g[i][j]=g[i-1][j];
                //初始化g表时,j-1位置(交易次数-1)合法时才参与计算
                if(j-1>=0) g[i][j]=Math.max(g[i-1][j],f[i-1][j-1]+prices[i]);
            }
        }
        //找出最大利润:
        int ret=0;
        for(int j=0;j<=k;j++){
            ret=Math.max(ret,g[n-1][j]);
        }
        return ret;
    }
}

总结

  • 复习解题思路和代码注释
相关推荐
吃好睡好便好1 小时前
矩阵秩的计算
人工智能·学习·线性代数·算法·机器学习·matlab·矩阵
计算机安禾1 小时前
【算法分析与设计】第35篇:后缀数据结构:后缀树与后缀数组的构造
大数据·人工智能·算法·机器学习·剪枝
计算机安禾1 小时前
【算法分析与设计】第38篇:最近点对与分治在几何中的应用
java·服务器·网络·数据库·算法
weixin_468466851 小时前
深度学习损失函数新手实战指南
人工智能·python·深度学习·算法·机器学习·ai
yzq1991271 小时前
语言在嵌入式系统中实现面向对象编程的实践与探索
算法
重生之我是Java开发战士1 小时前
【贪心算法】整数替换,俄罗斯套娃信封问题,可被三整除的最大和,距离相等的条形码,重构字符串
算法·贪心算法
小欣加油1 小时前
leetcode3633 最早完成陆地和水上游乐设施的时间I
数据结构·c++·算法·leetcode
memcpy01 小时前
LeetCode 2657. 找到两个数组的前缀公共数组【集合,位运算】中等
算法·leetcode·职场和发展
计算机安禾1 小时前
【算法分析与设计】第37篇:平面扫描与线段交问题
java·大数据·数据库·算法·机器学习