动态规划自顶而下与自底而上(递归)
参考labuladong算法
动态规划特点:
1 . 重叠子问题
2.状态转移方程
3.最优子结构
一般题目有上面三个特点基本就是动态规划了,主要是求最值,做题的核心是学会穷举,而好的算法是教我们学会更”聪明“的穷举。
动态规划解法代码框架:
//
#初始化(base case)
dp[0][0][...] = base
#进行状态转移
for 状态1 in 状态 1 的所有值:
for 状态2 in 状态2 的所有值:
for ...
dp[状态1][状态2][...] = 求最值 (选择1,选择2,...)
下来我们来看Leetcode322,题目为:
零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。你可以认为每种硬币的数量是无限的。
样例如下:
// An highlighted block
var foo = 'bar';
j示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
示例 4:
输入:coins = [1], amount = 1
输出:1
示例 5:
输入:coins = [1], amount = 2
输出:2
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 231 - 1
0 <= amount <= 104
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/coin-change
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
自顶而下算法:
题目要用最少数量的三枚硬币去凑11,因为有三种硬币,首先问题一定有最优解,所以可以转化为求分别凑10,9,6的子问题(subProblem), 10又可以分为凑9,8,5的子问题,以此类推。。。 经过穷举我们可以举出所有的情况,但如果如此暴力穷举,会造成效率低下,当硬币数量足够多,目标金额足够大,时间复杂度是指数级别的,这主要还是因为我们在不断的重复做一些之前已经做完的事情,所以我们要学会标记已经做过的东西,我们用memo数组来标记已经做好的事情,下一次遇到便能直接使用。代码如下:
// An highlighted block
class SolutionTwo
{
//此数组用于记录数据
int[] memo;
public int coinChange(int [] coins, int amount){
memo = new int[amount+1];
Arrays.fill(memo,-666);
// 将数组 memo 中每个元素都初始化为-666
return dp(coins,amount);
}
private int dp(int[] coins,int amount){
if(amount == 0) return 0;
// oK
if(amount < 0) return -1;
// 此方法行不通
if(memo[amount] != -666) return memo[amount];
//判断是否已经算出memo[amount]
int res = Integer.MAX_VALUE;
// 初始化结果
for(int coin:coins){
//计算子问题的结果
int subProblem = dp(coins, amount - coin); // 状态转移方程
//子问题无解则跳过
if(subProblem == -1) continue;
//在子问题中选择最优解,然后加一
res = Math.min(res,subProblem + 1);
}
//把计算结果存入数组中
memo[amount] = (res == Integer.MAX_VALUE) ? -1 : res;
return memo[amount];
}
}
部分递归过程如下:
强调文本
加粗文本 通过一部分穷举我们已求出拼凑1-5每个数字的最佳拼凑方案,即已经得到memo[1] 到 memo[5] ,当再次拼6时便可以直接拿来用,故经过一部分递归我们可以得到memo[1] 到 memo[11] 之后的每一次拼凑只需拿到memo[i]的值,极大的减少了重复的动作,提高了效率。
接下来我们再用自底而上去解决这个问题
自底而上:
dp[]数组的定义与上面类似,dp[i] = x 表示,当目标金额为i时,至少需要x枚硬币。依次算出dp【1】到dp【11】即可。
代码如下:
// An highlighted block
import java.util.Arrays;
class SolutionThree
{
public int coinChange(int[] coins, int amount){
int [] dp = new int[amount+1];
//dp数组全部初始化为特殊值amount + 1
Arrays.fill(dp,amount+1);
//base case
dp[0] = 0;
//外层for循环在遍历所有状态的所有取值
for(int i=0; i<dp.length; i++){
//内层for循环在求所有选择的最小值
for(int coin:coins){
//子问题无解
if(i-coin < 0) continue;
//状态转移
dp[i] = Math.min(dp[i],dp[i-coin]+1); // 状态转移方程
}
}
//判断能否凑出金额amount
return (dp[amount] == amount + 1) ? -1 : dp[amount];
}
public static void main(String[] args){
SolutionThree three = new SolutionThree();
int[] coins = {1,2,5};
System.out.println(three.coinChange(coins,11));
}
}
再用一道相似的例题去感受一下这种思想
题目如下:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
样例如下:
// An highlighted block
var foo = 'bar';
示例 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 。
提示:
0 <= nums.length <= 100
0 <= nums[i] <= 400
通过次数264,868提交次数
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/house-robber
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
自顶向下代码:
// An highlighted block
class RubberOne
{
int[] memo;
public int rob(int[] nums) {
memo = new int[nums.length];
Arrays.fill(memo,-1);
return dp(nums,0);
}
private int dp(int[] nums, int start){
if(start >= nums.length){
return 0;
}
if(memo[start] != -1){
return memo[start];
}
int res = Math.max(dp(nums,start+1), nums[start]+dp(nums,start+2));//状态转移方程
memo[start] = res;
return memo[start];
}
}
自底而上代码:
// An highlighted block
class Solution {
int rob(int[] nums){
int n = nums.length;
//base case : dp[n] = 0
int[] dp = new int[n+2];//最大为dp[n+1]
Arrays.fill(dp,0);
for(int i=n-1; i>=0; i--){
dp[i] = Math.max(dp[i+1],nums[i]+dp[i+2]);//状态转移方程
}
return dp[0];
}
}
注脚的解释
下面展示一些内联代码片
。 ↩︎