0、动态规划解题基本思路
找出三个关键要素:
- 最优子结构;
- 边界;
- 状态转换公式。
求解的三种方法:
- 递归;
- 备忘录:就是新开辟空间全部保存之前的结果;
- 动态规划:只需要保留状态转换时用到的那部分之前的结果。
示例:爬楼梯,N级楼梯,每次可以爬一级或者两级,问一共有多少种方法
边界:f(1) = 1; f(2) = 2;
最优子结构:f(3) = f(1) + f(2);
状态转换方程:f(n) = f(n-1) + f(n-2)。
求解:
- 递归:类似于斐波那契数列
- 备忘录:
public int count(int n){
if(n == 0){return 0;}
int[] preRes = new int[n];
for(int i = 0; i < n; i++){
if(i == 0){preRes[i] = 1;}
if(i == 1){preRes[i] = 2;}
if(i > 1){preRes[i] = preRes[i-1] + preRes[i-2];}
}
return preRes[n-1];
}
即新开辟O(n)的空间。
- 动态规划:我们观察状态转换方程可以发现,最终结果只与前两步有关,所以只需要开辟两个临时变量存储前两步的结果即可。
public int count(int n){
if(n == 0){return 0;}
int tmp1 = 1;
int tmp2 = 1;
int sum = 1;
for(int i = 1; i < n; i++){
sum = tmp1 + tmp2;
tmp1 = tmp2;
tmp2 = sum;
}
return sum;
}
1、力扣198 打家劫舍
1.1 题目描述
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
1.2 解析
首先考虑最简单的情况。如果只有一间房屋,则偷窃该房屋。如果只有两间房屋,只能偷窃其中的一间房屋,因此选择其中金额较高的房屋进行偷窃。
如果房屋数量大于两间,应该如何计算能够偷窃到的最高总金额呢?对于第 k~(k>2) 间房屋,有两个选项:
偷窃第 k 间房屋,那么就不能偷窃第 k-1 间房屋,偷窃总金额为前 k-2 间房屋的最高总金额与第 k 间房屋的金额之和。
不偷窃第 k 间房屋,偷窃总金额为前 k-1间房屋的最高总金额。
在两个选项中选择偷窃总金额较大的选项,该选项对应的偷窃总金额即为前 k 间房屋能偷窃到的最高总金额。
- 边界:
k = 1, f(0) = nums[0]; k = 2, f(1) = max(f(0), nums[1]);
- 最优子结构:
k = 3, f(2) = max(f(0) + nums[2], f(1));
- 状态转移方程:
f(k) = max(nums[k-1] + f(k - 2), f(k - 1));
状态转移中只涉及到两个之前的结果,所以申请两个临时变量即可。
public int rob(int[] nums) {
int sum = 0;
int len = nums.length;
if(len<1){
return 0;
}
int pre1 = 0;
int pre2 = 0;
for (int i = 0; i < len; i++) {
sum = Math.max(nums[i] + pre1,pre2);
pre1 = pre2;
pre2 = sum;
}
return sum;
}
2、279.完全平方数
2.1 问题描述
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
2.2 求解思路
悄咪咪说一句,我觉得动态规划就像是找规律,这个题我把前20个的数的组合全部写出来,观察规律,发现:
- 边界:
n = 完全平方数,f(n) = 1;
- 最优子结构:
f(5) = min(f(5-4) + 1,f(5-1) + 1);
- 状态转移方程:
f(n) = min(f(n-m) + 1),m为小于n的所有完全平方数
所以为了避免重复计算,我们开辟两个数组分别保存之前计算的所有结果和完全平方数。因为这个状态转移方程中并不确定最终要用到之前结果的第几个,所以要全部保存。
代码:
public int numSquares(int n) {
int[] preValue = new int[n+1];
ArrayList<Integer> squares = new ArrayList<>();
for(int i = 1, min = n; i <= n; i++,min = n){
int tmp = (int) (i/Math.sqrt(i));
if(tmp == Math.sqrt(i)){ // 判断是否为完全平方数
squares.add(i);
preValue[i] = 1;
}else {
for (Integer square : squares) {
min = Math.min(min, preValue[i - square] + 1); // 遍历所有的完全平方数,找最小的
}
preValue[i] = min;
}
}
return preValue[n];
}
3、322零钱兑换
3.1 问题描述
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
3.2 解析
和上述求完全平方数的思路一致,不一样的是这个题是给定了零钱的面值。
- 边界:
n = 其中一个零钱面值,f(n) = 1;
- 最优子结构:
f(5) = min(f(5-4) + 1,f(5-1) + 1);
- 状态转移方程:
f(n) = min(f(n-m) + 1),m为小于n的零钱面值
public int coinChange(int[] coins, int amount) {
int[] preValue = new int[amount+1];
Arrays.fill(preValue,amount+1); // 这里我原来是用的Inter.MaxValue填充的,后来发现存在溢出现象,还需要事先判断该位置是否匹配,多了一次运算,后来改进
preValue[0] = 0;
for(int i = 1; i <= amount; i++){
for(int j = 0; j < coins.length; j++){ // 这里我原来了把两个判断条件都放在for循环里,但是coins是无序的,只有遍历完coins才能退出循环
if(i >= coins[j]){
preValue[i] = Math.min(preValue[i],preValue[i - coins[j]]+1);
}
}
}
return preValue[amount]>amount?-1:preValue[amount];
}