动态规划(一)之打家劫舍
动态规划(一)之打家劫舍
做过算法题的小伙伴应该都知道动态规划吧,它无论是在ACM比赛还是在各大公司面试中都占有非常高的比重,可想而知动态规划在算法题中的地位。坏蛋哥在一开始就可以告诉小伙伴们,动态规划的题难的话可以非常难,简单的也可以so easy 的做出来,所以我分享的一般都是经典的题目,能把经典的吃透,再去挑战难度更大的肯定事半功倍。
今天分享的两个主角是打家劫舍(一)和打家劫舍(二)。爱问问题的小伙伴就要问了:那有没有打家劫舍(三)呢.......哈哈,那肯定是有的啦,不然怎么对得起坏蛋哥这讲解的智商呢(低调!)
动态规划的核心思想
说到动态规划,就不得不提一下递归了,坏蛋哥认为动态规划就是对递归的一种时间复杂度的优化,学过基本的算法的小伙伴肯定知道,要换取时间复杂度,那么很大可能就是要牺牲空间复杂度。是的,你没有听错。它的本质也就是用空间来换取时间。
动态规划重要的几个点:
1️⃣ 子问题(lamp sub-problem),也就是如何把一个复杂的问题分解。这儿也就引出了动态规划的重头戏--转移方程,后面我会讲解什么是转移方程
2️⃣ 数组,在动态规划的算法中,一般是用数组来作为辅助存储。所以我们要选择一个合适的辅助数组(比如是一维还是二维,数组的长度等等)
3️⃣ 计算顺序,举个简单的栗子:大家都应该知道斐波那契数列(Fibonacci sequence)吧,它是这样一个数列:0、1、1、2、3、5、8、13、21、34、……。是不是有这样的规律 f(i)=f(i-1)+f(i-2),i是数组的下标。试想如果我们先计算f(6),那么就要递归等待f(5)和f(4),然而f(5)又要递归f(4)和f(3)。是不是f(4)需要被递归多次。如果在f(i)非常大的时候,那么时间复杂度就会非常的高。但是我按照f(1),f(2),f(3)的顺序依次计算,结果又会怎样呢?很显然嘛,时间复杂度变为了O(n)。可想而知计算顺序的重要性。
4️⃣ 初始化,在后面的例子中坏蛋哥会解释这个过程。
有了上面的知识储备,那么就让我们实际来解题试试(疗效好不好,事实说了算)
打家劫舍一
这道题是leetcode上面比较经典的动态规划题,获取原题(https://leetcode-cn.com/problems/house-robber/)
在解动态规划算法的时候,某些题正好是选与不选的概念。
具体解释是这样的,我们换一种思路:我们要得到最大值,是不是就涉及到一个“选与不选”的概念,比如说上面有四个元素,我们依次标为a[0],a[1],a[2],a[3]。是不是我选了a[3],就不能选a[2]了,就只能选a[1]和a[1]之前的元素呢?
本题的题解思路:
1️⃣ 找出转移方程(又一个大的状态转移到一个小的状态):dp[i]=max( f(i) + dp[i-2] ,dp[i-1] )
注:解释一下参数:dp[i] 表示0~i的数组偷取的最大money,f(i)便是第i个元素可以偷取的money。
2️⃣ 计算方向:很显然这个问题类似于斐波拉契数列,如果从后往前,那么是需要递归的,如果从前往后,那么就是一条路算到底。复杂度就变为了O(n)。
3️⃣ 初始化:既然是从前往后,但是第一个元素的前两个元素怎么表示呢? 其实做法很简单,将数据前面加两个值为0的元素不就解决问题了吗。
流程拉通下来,小伙伴们是不是感觉原来都是套路啊!
是的,但是也有不按套路出牌的,但是是少数哦,那些能难到你怀疑人生。
最后我们就看一下具体的代码:
import static java.lang.Math.max;
class Solution {
public int rob(int[] nums) {
//特判
if (nums==null && nums.length==0){
return 0;
}
//初始化变量
int[] maxMoney=new int[nums.length+2];
//开始计算
for (int i=0;i<nums.length;i++){
//maxMoney的i+2与nums相对应
maxMoney[i+2]=max(maxMoney[i]+nums[i],maxMoney[i+1]);
}
return maxMoney[maxMoney.length-1];
}
}
get到了吗,是不是感觉坏蛋哥的讲解这么的到位。是不是有想👍的冲动。
打家劫舍(二)
完成上面的练习,那恭喜你们,已经获得解决动态规划的一个概念,那么剩下就是通过更多的题目来体会上面的要点。那么我们就继续打家劫舍(哈哈,坏蛋哥是个好人,不是真的要打家劫舍哈。)
甩出题目:如需原题,请访问https://leetcode-cn.com/problems/house-robber-ii/
这道题怎么做呢,感觉又要掉好多头发。哈哈,我们需要结合偷前面的money的经验哦(毕竟小偷这个职业也需要汲取历史的经验嘛)。
我们是否单独考虑最后一个元素呢,如果选择最后一个元素,那么就不能选择第一个和最后一个的前一个元素。如果不选呢?是不是从第一个到倒数第二个就是和打家劫舍一相同了。是的,通过上面的分析,我们就讲问题转换为上一个问题了。
具体题解代码:
package com.yunhai.leetcode.dynamic_prgraming;
import static java.lang.Integer.max;
/**
* function:实现打家劫舍二
* 打家劫舍二可以转换为打家劫舍二,当最后一个元素选择时,那么就从1到(n-2)为劫舍一的问题
* 当最后一个元素不要的时候,那么就是从0到n-1为劫舍二的问题
*
*/
public class Rob213 {
public int rob(int[] nums) {
int len = nums.length;
int maxMoney = 0;
//特判
if (len <= 3) {
if (len == 0) {
maxMoney = 0;
} else {
for (int i = 0; i < len; i++) {
maxMoney = max(maxMoney, nums[i]);
}
}
System.out.println("长度小于三,且maxMoney为:" + maxMoney);
return maxMoney;
}
//将问题拆分
int[] eliminateFinal = new int[len - 1 + 2]; //前面加两个辅助元素,并都置为0
int[] choseFinal = new int[len - 2 + 2];
//初始化
eliminateFinal[0] = eliminateFinal[1] = 0;
choseFinal[0] = choseFinal[1] = 0;
//开始计算elimateFinal 0到n-1
for (int i = 0; i < len - 1; i++) {
eliminateFinal[i + 2] = max(nums[i] + eliminateFinal[i + 2 - 2], eliminateFinal[i + 2 - 1]);
System.out.print(eliminateFinal[i+2]+"\t");
}
System.out.println();
//开始计算choseFinal 1到n-2
for (int i = 1; i < len - 2; i++) {
choseFinal[i - 1 + 2] = max(nums[i] + choseFinal[i + 2 - 2 - 1], choseFinal[i + 2 - 1 - 1]);
//上面之所以要减一,是因为i是重1开始的
System.out.print(choseFinal[i-1+2]+"\t");
}
System.out.println();
//System.out.println(eliminateFinal[len - 2 + 2]+"##"+choseFinal[len - 4 + 2] + nums[len - 1]);
//比较最后的结果
return max(eliminateFinal[len - 2 + 2], choseFinal[len - 4 + 2] + nums[len - 1]);
}
/**
* 测试:
*{1,2,1,1};
*
* @param args
*/
public static void main(String[] args){
int[] test={1,3,1,3,100};
Rob213 rob213=new Rob213();
System.out.println(String.format("结果为:%d", rob213.rob(test)));
}
}
总结
相信经过上面坏蛋哥经典(垃圾)的讲解,小伙伴们已经会解决一般的动态规划的问题,坏蛋哥其实挺喜欢刷动态规划的题,因为他真的有时候很操蛋(难到要死),但是看了大神的思路就欣喜若狂。本来学习就是这样,我们要好好享受由不会到会的这种快感,与人交流,相互学习,共同进步。
永做爱学习的小白,我是坏蛋哥,咱们下次再会。如果你也喜欢坏蛋哥的作品,记得👍加关注哦。