当前位置: 首页 > ai >正文

《多状态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]));}
}
http://www.xdnf.cn/news/10609.html

相关文章:

  • Leetcode 1136. 并行课程
  • MySQL语法练习 - 基础DDL/DML/DQL/DCL练习
  • 监督学习 vs 无监督学习:AI两大学习范式深度解析
  • Java内部类详细教程
  • 06.MySQL数据库操作详解
  • Retrievers检索器+RAG文档助手项目实战
  • 字符串加解密
  • 配置刷新技术
  • 【Python 进阶3】常见的 call 和 forward 区别
  • JavaSE 字符串:深入解析 String、StringBuilder与 StringBuffer
  • 第十章:Next的Seo实践
  • 力扣HOT100之多维动态规划:62. 不同路径
  • C. Basketball Exercise
  • Vue-6-前端框架Vue之基于Plotly.js绘制曲线
  • 3,信号与槽机制
  • BUUCTF[ACTF2020 新生赛]Include 1题解
  • NVM,Node.Js 管理工具
  • 【Delphi】接收windows文件夹中文件拖拽
  • (Python网络爬虫);抓取B站404页面小漫画
  • Python-matplotlib库之核心对象
  • 设计模式——备忘录设计模式(行为型)
  • Kotlin 中 companion object 扩展函数详解
  • Java连接Redis和基础操作命令
  • 【Linux】Ubuntu 20.04 英文系统显示中文字体异常
  • 什么是线程上下文切换?
  • 【SpringBoot】| 接口架构风格—RESTful
  • 概率统计:AI大模型的数学支柱
  • Linux--进程概念
  • 【redis实战篇】第七天
  • 03- javascript的运行原理