算法基础——动态规划和打家劫舍问题
编辑时间:2023/10/10
1.动态规划
基本思想
1.动态规划算法基本思想:
将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。
2.动态规划法的基本思路:
我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是。
适用条件
适用动态规划的问题必须满足最优化原理和无后效性。
1.最优化原理(最优子结构性质):
一个最优化策略的子策略总是最优的,一个问题满足最优化原理又称其具有最优子结构性质 。
2.无后效性:
每个状态都是过去历史的一个完整总结,这就是无后向性,又称为无后效性 。
3.子问题的重叠性:
动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其他的算法。
2.打家劫舍问题(单排排列)
问题描述
力扣题目【198】
目标:每个房屋有对应的值,求能够偷窃到的最高累计金额。
约束条件:单排排列的房屋;相邻的房屋不能偷取。
解题思路
递归加创建备忘录的动态规划方法。
由于相邻的房子不能偷取故房子的解题思路如下表所示:
我们可以将原问题划分为子问题,可以发现无论前几项的选择是什么在最后的选择里,第n项、第n-1项和第n-2项存在关系。
算法描述
若用递归函数的方法,如下代码所示
package PlunderHouses;
public class Rob {
public static void main(String[] args) {
//调用递归函数
int nums[]=new int[]{1,2,3,4,5,6};
System.out.println(maxMoney(nums,nums.length-1));
}
//创建递归方法maxMoney() 参数:数组nums[] 下标index
static int maxMoney(int nums[],int index){
//递归出口 数组为空 长度为负数 返回
if(nums==null||index<0)
return 0;
//递归出口 数组长度为1 返回改数组的nums[0]
if(index==0)
return nums[0];
//递归函数+动态规划
// nums[index]+max(nums[index-2]) or max(nums[index-1])
//5 7 1-1
return Math.max(maxMoney(nums,index-1),maxMoney(nums,index-2)+nums[index]);
}
}
动态规划创建备忘录方法:
package PlunderHouses;
public class Rob {
public static void main(String[] args) {
int nums[]=new int[]{1,2,3,4,5,6};
System.out.println(maxMoney(nums));
}
static int maxMoney(int nums[]){
int length=nums.length;
if(nums==null||length<0)
return 0;
if(length==1)
return nums[0];
//创建备忘录
int dp[]=new int[nums.length];
//备忘录为0 1的记录
dp[0]=nums[0];
dp[1]=Math.max(nums[0],nums[1]);
//遍历备忘录
for(int i=2;i<length;i++){
dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[length-1];
}
}
优化空间复杂度将空间复杂度从O(n)到O(1)的代码如下:
package PlunderHouses;
public class Rob {
public static void main(String[] args) {
int nums[] = new int[]{1, 2, 3, 4, 5, 6};
System.out.println(maxMoney(nums));
}
static int maxMoney(int nums[]) {
int length = nums.length;
if (nums == null || length < 0)
return 0;
if (length == 1)
return nums[0];
//优化空间复杂度算法 设计中转变量让指针流动
int first = nums[0];
int second = nums[1];
for (int i = 2; i < length; i++) {
int temp = second;
second = Math.max(second, first + nums[i]);
first = temp;
}
return second;
}
}
时间及空间复杂度分析
时间复杂度:O(n)
空间复杂度:O(1)
3.打家劫舍问题(首尾相连)
问题描述
力扣题目【213】https://leetcode.cn/problems/house-robber-ii/
目标:每个房屋有对应的值,求能够偷窃到的最高累计金额。
约束条件:环状排列的房屋;相邻的房屋不能偷取。
解题思路
情况1:抢头不抢尾
情况2:抢尾不抢头
情况3:不抢头不抢尾
分析可得,不抢头不抢尾相当于初版的打家劫舍,考虑的最优解是被包含在情况1和情况2中,故此我们只求对情况1和情况2各自的最优解的最大值。什么只需要对考虑的值的那一段传入到封装好的初版的打家劫舍的函数里就可以。
算法描述
package PlunderHouses;
public class Rob {
public static void main(String[] args) {
int nums[] = new int[]{1, 2, 3, 4, 5, 6};
//nums,0,nums.length-2
//nums,1, nums.length-1
System.out.println(Math.max(maxMoney(nums,0,nums.length-2),maxMoney(nums,1, nums.length-1)));
}
static int maxMoney(int nums[],int start,int end ) {
int length = nums.length;
if (nums == null || length < 0)
return 0;
if (length == 1)
return nums[0];
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;
}
}
时间及空间复杂度分析
遍历的时间复杂度为O(n),优化后的子结构空间复杂度为O(1)。
讨论和结果
线性规划和分治法的相似之处在于他们都将原问题转化为若干个子问题,但不同在于“动态”的变化,故此可以创建一个备忘录来存储这些变化的值,是以牺牲空间换时间来获得的。在环形的“打家劫舍”中,我们需要对递归的出口分析,根据问题分类为考虑头或者考虑尾的问题,创建备忘录来获取最大的最优解。