《多状态DP:状态设计与状态转移方程速成指南》
1.按摩师
题目链接:面试题 17.16. 按摩师 - 力扣(LeetCode)
题目描述:从一个预约请求队列中,找出一个总预约时间最长的预约集合,不能选择相邻位置的预约
算法讲解:动态规划
1.状态表示:经验+题目要求
经验就是以i位置为起点或者以i位置为结尾,什么什么
在这道题中,根据题目要求和经验,dp[i]的状态表示为:
dp[i]表示:选择到i位置时,此时的最长预约时长
此时还可以继续细分状态表示,由于既可以选择第i个位置的预约,也可以不选择第i个位置的预约,此时就会有两种情况
用f[i]表示选择到第i个位置时,选择第i个位置的预约时的最长预约总时长
用g[i]表示选择到第i个位置时,不选择第i个位置的预约时的最长预约总时长
2.状态转移方程
第一种情况:选择第i个位置的预约
此时,由于选择了第i个位置的预约,所以就不能再选择第i-1个位置上的预约,此时要求f[i]的话,首先要知道不选择第i-1个预约时的最长预约总时长, 根据细分的状态表示,可以用g[i-1]表示不选择第i-1个预约时的最长预约总时长,所以此时:f[i]=g[i-1]+nums[i]
第二情况:不选择第i个位置的预约
此时,由于不选择第i个位置的预约,所以在第i-1个位置的预约选择上,可以选择第i-1个位置上的预约,也可以不选择第i-1个位置上的预约,所以此时计算g[i]也会有两种情况
计算g[i]的第一种情况:选择第i-1个预约
此时计算g[i]时,就要知道选择到第i-1个预约时,选择第i-1个位置的预约时的最长总预约时长,根据状态表示,可以用f[i-1]来表示选择到第i-1个预约时,选择第i-1个位置的预约时的最长总预约时长,此时g[i]=f[i-1]
计算g[i]的第二种情况:不选择第i-1个预约
此时计算g[i]时,就要知道选择到第i-1个预约时,不选择第i-1个位置的预约时的最长总预约时长,根据状态表示,可以用g[i-1]来表示选择到第i-1个预约时,选择第i-1个位置的预约时的最长总预约时长,此时g[i]=g[i-1]
由于要计算最长的总预约时长,所以最终g[i]=max(f[i-1],g[i-1])
3.初始化
要初始化f[0]和g[0],根据状态表示,将f[0]初始化为nums[0]接口,将g[0]初始化为0
4.填表顺序
从左往右同时填f表和g表即可
5.返回值
返回max(f[n-1],g[n-1])即可
代码实现:
class Solution {public int massage(int[] nums) {int n=nums.length;if(n==0) return 0;int[] f=new int[n];int[] g=new int[n];f[0]=nums[0];g[0]=0;for(int i=1;i<n;i++){f[i]=g[i-1]+nums[i];g[i]=Math.max(f[i-1],g[i-1]);}return Math.max(f[n-1],g[n-1]);}
}
2.打家劫舍 I
题目链接:LCR 089. 打家劫舍 - 力扣(LeetCode)
题目描述:一个小偷在不能偷相邻房屋的现金时,返回小偷偷到最后一个房屋时能偷到的最大金额。
算法原理:
1.状态表示:经验+题目要求
在这道题中,可以将偷到第i个位置时,此时的偷到的最大金额,但是偷到第i个位置时,小偷是可以选择偷第i个位置或者不偷第i个位置的,所以状态表示可以细分为两种
f[i]表示:偷到第i个位置时,偷nums[i],此时的最大金额
g[i]表示:偷到第i个位置时,不偷nums[i],此时的最大金额
2.状态转移方程
推算状态转移方程时也会有两种情况
第一种情况:偷第i个位置的房屋
此时,由于偷了第i个位置时的房屋,所以就不能再去偷第i-1个位置上的房屋了,此时要推算f[i]的话,首先要知道不偷第i-1个位置的房屋时能偷到的最大金额,根据状态表示,可以用g[i-1]表示不偷第i-1个位置时能偷到的最大金额,所以此时:f[i]=g[i-1]+nums[i]
第二种情况: 不偷第i个位置的房屋
此时,由于不偷第i个位置的房屋,所以在偷第i-1个房屋的选择上,是可以选择偷第i-1个位置的房屋,也可以选择不偷第i-1个位置的房屋,所以,此时计算g[i]也会有两种情况
计算g[i]的第一种情况:偷第i-1个位置的房屋
此时,选择偷第i-1个位置的房屋,所以此时计算g[i]要知道偷到第i-1个位置时能得到的最大金额,根据状态表示,可以用f[i-1]来表示偷到第i-1个位置的房屋时得到的最大金额,所以,g[i]=f[i-1]
计算g[i]的第二种情况:不偷第i-1个位置的房屋
不选择偷第i-1个位置的房屋,所以此时计算g[i]要知道不偷到第i-1个位置时能得到的最大金额,根据状态表示,可以用g[i-1]来表示不偷到第i-1个位置的房屋时得到的最大金额,所以,g[i]=g[i-1]
由于要求的是最大金额,所以最终g[i]=Math.max(f[i-1],g[i-1])
3.初始化
根据状态表示,我们要去初始化f[0]和g[0],f[0]表示偷到第一个房屋时能够偷到的最大金额,此时就将f[0]初始化为nums[0],也就是f[0]=nums[0],同理,根据g[i]的状态表示,要将g[0]初始化为0
4.填表顺序
从左往右,同时填f表和g表
5.返回值
返回max(f[n-1],g[n-1])即可
代码实现:
class Solution {public int rob(int[] nums) {int n=nums.length;int[] f=new int[n];int[]g=new int[n];f[0]=nums[0];g[0]=0;for(int i=1;i<n;i++){f[i]=g[i-1]+nums[i];g[i]=Math.max(f[i-1],g[i-1]);}return Math.max(f[n-1],g[n-1]);}
}
3.打家劫舍 II ---转换为打家劫舍问题
题目链接:213. 打家劫舍 II - 力扣(LeetCode)
这道题和打家劫舍 I 非常相似,只是在这道题中有一个不同点:第一个位置的房屋和最后一个位置的房屋也是相邻的,但小偷偷了第一个房屋,就不能盗窃最后一个房屋了,所以此时会有两种情况
第一种情况:偷第1个位置上的房屋
此时,如果偷了第1个位置上的房屋,那么第2个位置的房屋和最后一个位置的房屋都不能偷了,所以此时,只要计算从第3个位置的房屋偷到倒数第二个位置的房屋时能偷到的最大金额(运用打家劫舍 I 的思路) ,然后再加上nums[0]的金额就是第一种情况的最大金额
第二种情况:不偷第1个位置的房屋
此时,如果不偷第1个位置上的房屋,那么此时计算从第2个位置的房屋都到最后一个房屋时的最大金额即可(运用打家劫舍 I 的思路)
代码实现:
class Solution {public int rob(int[] nums) {int n=nums.length;return Math.max(nums[0]+rob1(nums,2,n-2),rob1(nums,1,n-1));}public int rob1(int[] nums,int left,int right){if(left>right) return 0;int n=nums.length;int[] f=new int[n];int[] g=new int[n];f[left]=nums[left];for(int i=left;i<=right;i++){f[i]=g[i-1]+nums[i];g[i]=Math.max(g[i-1],f[i-1]);}return Math.max(f[right],g[right]);}
}//我写的版本
class Solution {public int rob(int[] nums) {int n=nums.length;if(n==1) return nums[0];if(n==2) return Math.max(nums[0],nums[1]);int[] f1=new int[n];int[] g1=new int[n];int[] f2=new int[n];int[] g2=new int[n];f1[2]=nums[2];for(int i=2;i<n-1;i++){f1[i]=g1[i-1]+nums[i];g1[i]=Math.max(f1[i-1],g1[i-1]);}int ret1=Math.max(f1[n-2],g1[n-2])+nums[0];f2[1]=nums[1];for(int i=1;i<n;i++){f2[i]=g2[i-1]+nums[i];g2[i]=Math.max(f2[i-1],g2[i-1]);}int ret2=Math.max(f2[n-1],g2[n-1]);return Math.max(ret1,ret2);}
}
4.删除并获得点数---转换为打家劫舍问题
题目链接:740. 删除并获得点数 - 力扣(LeetCode)
题目解析:
这道题依旧是打家劫舍类型的题目,根据题目描述,当选择x数字时,x+1数字和x-1数字是不能被选择的,就像打家劫舍问题中选择i位置的金额后就不能选择i+1位置的金额和i+1位置的金额。
如果在这道题中,nums=【1,2,3,4,5】,那么此时这道题就直接用打家劫舍的思路解决即可,但是nums有可能不是连续的情况,例如nums=【1,2,2,4,4,5,5,7,9,9,9】,但是可以将nums数组的数据映射到一个arr数组中,其中,在arr数组中,i表示nums[i],arr[i]表示 “i这个数字出现的总和”,此时数组的下标是有序到的,所以此时直接对arr数组进行一次打家劫舍就行了
算法讲解:
1.状态表示:
用f[i]表示:选到i位置时,arr[i]必选,此时能够获得的最大点数
用g[i]表示:选到i位置时,arr[i]不选,此时能够获得的最大点数
2.状态转移方程:
f[i]=g[i-1]+nums[i],g[i]=Math.max(f[i-1],g[i-1])
3.初始化:
将f[0]=arr[0],g[0]=0
4.填表顺序
从左往右,同时填两个表即可
5.返回值
返回max(f[n-1],g[n-1])即可
代码实现:
class Solution {public int deleteAndEarn(int[] nums) {int n=nums.length;int max=nums[0];for(int x:nums){if(x>max) max=x;}int[] arr=new int[max+1];for(int i=0;i<n;i++)arr[nums[i]]+=nums[i];int[] f=new int[max+1];int[] g=new int[max+1];//初始化f[0]=arr[0];for(int i=1;i<max+1;i++){f[i]=g[i-1]+arr[i];g[i]=Math.max(f[i-1],g[i-1]);}return Math.max(f[max],g[max]); }
}
5.粉刷房子
题目链接:LCR 091. 粉刷房子 - 力扣(LeetCode)
题目解析:相邻的房子不能涂相同的颜色,计算并返回涂刷完所有房子的最小费用
算法讲解:
1.状态表示:经验+题目要求
这道题中,可以将 粉刷到第i个房子时,花费的最小费用 作为 状态表示,但是此时还可以细分状态表示,当涂刷到第i个房子时,是有红蓝绿三种颜色选择的,所以细分的状态表示:
dp[i][0]表示:粉刷到第i个房子时,选择刷红色,此时花费的最小费用
dp[i][1]表示: 粉刷到第i个房子时,选择刷蓝色,此时花费的最小费用
dp[i][2]表示:粉刷到第i个房子时,选择刷绿色,此时花费的最小费用
2.状态转移方程
由于有3种状态表示,所以推状态转移方程也会有3中情况,但是推一种情况就行了,另外两种情况推算的逻辑一样,下面,将以推算dp[i][0]为例,也就是以红色为例
dp[i][0]表示将会第i个位置的房子刷成红色,此时花费的最小费用,如果想要求出dp[i][0],此时就必须要知道粉刷到第i-1个房子时花费的最小费用,此时因为第i个房子粉刷成红色了。所以此时第i-1个位置的房屋只会有粉刷蓝色或者绿色这两种情况,所以此时就要知道 粉刷到第i-1个房子并且将房子粉刷成蓝色的最小花费 或者 粉刷到第i-1个房子并且将房子粉刷成绿色的最小花费,由于要求的是最小花费,所以此时的状态转移方程:
dp[i][0]=Math.min(dp[i-1][1],dp[i-1][2])
同理可得:其他两种情况的状态转移方程为:
将第i个房子刷成蓝色:dp[i][1]=Math.min(dp[i-1][0],dp[i-1][2])
将第i个房子刷成绿色:dp[i][1]=Math.min(dp[i-1][0],dp[i-1][1])
3.初始化
初始化时,要进行dp[0][0]=cost[0][0],dp[1][1]=cost[1][0],dp[2][2]=cost[2][0]这些初始化的操作,为了方便初始化,可以在创建dp表时,多创建一列的空间,此时就有两个注意事项:
第一个注意事项:多出来的空间里填的值,要保证后面的填表正确,此时根据题目分析,将多出来的空间全部填0即可
第二个注意事项:注意下标的映射关系
4.填表顺序:
从左往右,同时填三个表即可
5.返回值
返回min(dp[n][0],dp[n][1],dp[n][2])即可
代码实现:
class Solution {public int minCost(int[][] cost) {//房子数量int n=cost.length;int[][] dp=new int[n+1][3];for(int i=1;i<n+1;i++){dp[i][0]=Math.min(dp[i-1][1],dp[i-1][2])+cost[i-1][0];dp[i][1]=Math.min(dp[i-1][0],dp[i-1][2])+cost[i-1][1];dp[i][2]=Math.min(dp[i-1][0],dp[i-1][1])+cost[i-1][2];}return Math.min(dp[n][0],Math.min(dp[n][1],dp[n][2]));}
}