java数据结构与算法刷题目录(剑指Offer、LeetCode、ACM)-----主目录-----持续更新(进不去说明我没写完):https://blog.csdn.net/grd_java/article/details/123063846 |
---|
很多人觉得动态规划很难,但它就是固定套路而已。其实动态规划只不过是将多余的步骤,提前放到dp数组中(就是一个数组,只不过大家都叫它dp),达到空间换时间的效果。它仅仅只是一种优化思路,因此它目前的境地和线性代数一样----虚假的难。
- 想想线性代数,在国外留学的学生大多数不觉得线性代数难理解。但是中国的学生学习线性代数时,完全摸不着头脑,一上来就是行列式和矩阵,根本不知道这玩意是干嘛的。
- 线性代数从根本上是在空间上研究向量,抽象上研究线性关系的学科。人家国外的教科书都是第一讲就帮助大家理解研究向量和线性关系。
- 反观国内的教材,直接把行列式搞到第一章。搞的国内的学生在学习线性代数的时候,只会觉得一知半解,觉得麻烦,完全不知道这玩意学来干什么。当苦尽甘来终于理解线性代数时干什么的时候,发现人家国外的教材第一节就把这玩意讲清楚了。你只会大骂我们国内这些教材,什么狗东西(以上是自己学完线性代数后的吐槽,我们同学无一例外都这么觉得)。
而我想告诉你,动态规划和线性代数一样,我学完了才知道,它不过就是研究空间换时间,提前将固定的重复操作规划到dp数组中,而不用暴力求解,从而让效率极大提升。
- 但是网上教动态规划的兄弟们,你直接给一个动态方程是怎么回事?和线性代数,一上来就教行列式和矩阵一样,纯属恶心人。我差不多做了30多道动态规划题目,才理解,动态方程只是一个步骤而已,而这已经浪费我很长时间了,我每道题都一知半解不理解,过程及其痛苦。最后只能重新做。
- 动态规划,一定是优先考虑重复操作与dp数组之间的关系,搞清楚后,再提出动态方程。而你们前面步骤省略了不讲,一上来给个方程,不是纯属扯淡吗?
- 我推荐研究动态规划题目,按5个步骤,从上到下依次来分析
- DP数组及下标含义
- 递推公式
- dp数组初始化
- 数组遍历顺序(双重循环及以上时,才考虑)
- dp数组打印,分析思路是否正确(相当于做完题,检查一下)
这道题是198题的衍生题,在198题的基础上,增加了一个条件,就是第一个房子和最后一栋房子是挨着的。如果偷了第一个房子,那么最后一个房子就不能偷。除此之外没有任何不同
可以先参考🏆LeetCode198. 打家劫舍https://blog.csdn.net/grd_java/article/details/135401880 |
---|
先理解题目细节 |
---|
想象这样一个场景,我们是小偷,从第一栋挨个考虑偷还是不偷(
注意:不考虑后面的,只考虑当前这栋和之前的
),如果偷,那就是身上已经偷了多少+这一栋能偷多少。如果不偷,那就是身上已经偷了多少就是还是多少。但是相邻的不能同时偷,主要是第一栋如果偷了,最后一栋就不能偷。
- 题目给我们的数组代表每个房子有多少钱,而且相邻的房子不能都偷,只能偷不相邻的。
而且首尾相邻,若要偷第一栋,则最后一栋不能偷,不偷第一栋则最后一栋可以偷
- 例如2,3,2 这个序列,显然不触发警报的前提下(不偷相邻的)----偷第二个为能偷到的最多的钱。
不可以偷第一栋和第3栋=4元,因为首尾相邻
- 怎么偷才能偷到最多呢?当然是保证最大获利情况下,能偷的都偷了,而且不能触发警报。
- 我们每个房子都单独考虑,偷还是不偷,而且只需考虑它前面的房子,因为我们要能偷的都偷
- 若要偷当前房子:那么,前面相邻的不能考虑,只能考虑再前面一个,也就是当前房子的金额,加上除了前面一个相邻的房子外,已经偷了多少。
- 若不偷当前房子:那么,前面偷了多少,到这个房子就还有多少,金额不变
解题思路 |
---|
- 暴力求解的思想,就是回溯算法,枚举每一种情况,拿到最大值,显然会做大量无效运算。
- 但是如果我们预先将其存储到dp数组,就可以直接通过dp[x], 获取dp数组中指定位置x的体力花费,而不用枚举。典型的动态规划题目
动态规划思考5步曲 |
---|
- DP数组及下标含义
我们
要求出的是
到了某个房子后最大情况下偷了多少钱,那么dp数组中存储的
就是最大情况下偷了多少钱。要求出谁的
最大情况下偷了多少。显然是到达某个房子后,那么下标就是代表现在到了哪个房子
,也就是代表到了某一栋房子
后的最大已偷取的金额。显然,只需要一个下标即可表示,故这道题的dp数组只需要一维数组
- 递推公式
- 由题意可知,每个房子都可以选择偷与不偷,选择最大值。而我们选择要偷的第一个房子肯定要偷它,才能获得最大值。而第二个房子,因为它前面没有不相邻的房子,所以它要么不偷,也就是只偷第一个房子。要么选择偷第二个房子,而第一个房子不偷,然后选择最大情况。
故:第一个房子固定为F(start) = 第一个房子金额。第二个固定为F(start+1) = max(第一个房子金额,第二个房子金额)
请
注意
:上面说的第一个房子,是相对于小偷来说,他偷的第一个房子。例如整个街区中,我们不偷第一栋,那么就从第二栋开始偷。对于小偷来说,这第二栋就是他偷的第一个房子,记为start.
- 之后每一栋,都需要
判断它偷还是不偷,以及前面不相邻的房子
。如果偷,就要考虑前面不相邻的+自己这栋能偷多少钱。不偷,那么前一栋相邻的它肯定考虑一下。而前面的房子怎么考虑的是前面房子的事,它们也考虑过了偷还是不偷。我们只考虑当前这栋是偷,还是不偷- 因此,可以得到,从第三栋开始(相对于小偷来说),递推公式为偷或者不偷,选择最大值: F(n) = max( F(n-2)+nums[n] , F(n-1) )
- dp数组初始化
- 数组遍历顺序(因为这个数列是一维的,只需要一重循环,无需考虑这个)
- 打印dp数组(自己生成dp数组后,将dp数组输出看看,是否和自己预想的一样。)
代码:时间复杂度O(n).空间复杂度O(n) |
---|
class Solution {
public int rob(int[] nums) {
int length = nums.length;//房子的数量
if(length == 1) return nums[0];//如果只有一栋,那么肯定偷它,返回即可
else if (length == 2) return Math.max(nums[0],nums[1]);//如果只有两栋,那么相邻的不能偷,只能选一栋钱多的
//如果房子很多,那么分为两种情况①:第一栋要偷,最后一栋不能偷。②:第一栋不偷,最后一栋可以偷
return Math.max(robRange(nums,0,length-2), robRange(nums,1,length-1));
}
public int robRange(int[] nums, int start, int end){
/**进入小偷视角,他第一个偷的房子start,为他眼里的第一栋。 而实际在街区可能是第一栋,也可能是第二栋**/
int dp[] = new int [nums.length];//dp数组
dp[start] = nums[start];//只考虑第一栋的情况下,第一栋只有偷了,才是最好的选择
dp[start+1] = Math.max(nums[start],nums[start+1]);//只考虑前两栋的情况下,可以选择偷第一栋,或者偷第二栋
//剩下的都需要考虑①:偷还是不偷,②:如果偷=前面不相邻的已经偷了多少+当前这栋有多少。③:如果不偷,那么前面已经偷了多少,就是多少
for(int i = start+2; i<= end;i++){
dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[end];
}
}
学有余力的同学可以尝试这个方法,将空间复杂度变为常数级-----------------代码:时间复杂度O(n).空间复杂度O(1) |
---|
将dp数组优化掉,换成3个变量,滚动执行。将dp[0]换成first。dp[1]换成second. 也就是first永远指向当前栋的前面不相邻的,second永远指向前面相邻的。
class Solution {
public int rob(int[] nums) {
int length = nums.length;//房子的数量
if(length == 1) return nums[0];//如果只有一栋,那么肯定偷它,返回即可
else if (length == 2) return Math.max(nums[0],nums[1]);//如果只有两栋,那么相邻的不能偷,只能选一栋钱多的
//如果房子很多,那么分为两种情况①:第一栋要偷,最后一栋不能偷。②:第一栋不偷,最后一栋可以偷
return Math.max(robRange(nums,0,length-2), robRange(nums,1,length-1));
}
public int robRange(int[] nums, int start, int end){
/**进入小偷视角,他第一个偷的房子start,为他眼里的第一栋。 而实际在街区可能是第一栋,也可能是第二栋**/
int dp[] = new int [nums.length];//dp数组
int first = nums[start];//只考虑第一栋的情况下,第一栋只有偷了,才是最好的选择
int second = Math.max(nums[start],nums[start+1]);//只考虑前两栋的情况下,可以选择偷第一栋,或者偷第二栋
//剩下的都需要考虑①:偷还是不偷,②:如果偷=前面不相邻的已经偷了多少+当前这栋有多少。③:如果不偷,那么前面已经偷了多少,就是多少
for(int i = start+2; i<= end;i++){
int temp = second;
second = Math.max(first+nums[i],second);
first = temp;
}
return second;
}
}