一维动态规划
70.爬楼梯(⭐)
题目描述:
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
思路分析:
我们用f(x)来表示爬到x级楼梯的方案数,由于每次只能走一阶或两阶楼梯,所以我们只需要算出f(x-1)与f(x-2),并把其加和,就得到了f(x)的值:f(x) = f(x-1) +f(x-2)。怎么理解呢?由于每次只能走一阶或两阶楼梯,所以当我们爬上第x阶楼梯时,上一步必然是从x-1阶或者x-2阶走过来的,并且要统计所有的方案总数,所以两项加和就可以得到最终的方案总数。
代码:(空间复杂度O(n),时间复杂度O(n))
class Solution {
public int climbStairs(int n) {
if(n == 1 ||n == 2){
return n;
}
int[] dp = new int[n+1];
dp[1] = 1;
dp[2] = 2;
for(int i = 3;i <= n;i++){
dp[i] = dp[i-1]+dp[i-2];
}
return dp[n];
}
}
代码优化:
刚刚代码定义了一个长度为n+1的dp数组用于存储方案数,dp[n]就代表爬上n阶楼梯需要的方案总数。但是我们注意到,当我们计算dp[n]时,我们只用到了dp[n-1]与dp[n-2],前边存储的数据根本没有用处了。浪费存储空间。解决方案就是定义两个变量用于存储dp[n-1]与dp[n-2]的值,通过每次计算完将两个变量的值进行替换来达到效果。
优化后代码:(空间复杂度O(1),时间复杂度O(n))
class Solution {
public int climbStairs(int n) {
if(n == 1 ||n == 2){
return n;
}
int sum = 0, first = 1,last = 2;
for(int i = 3;i <= n;i++){
sum = first + last;
first = last;
last = sum;
}
return sum;
}
}
322.零钱兑换(⭐⭐)
题目描述:
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
。
你可以认为每种硬币的数量是无限的。
思路分析:
1、确定 base case,这个很简单,显然目标金额 amount
为 0 时算法返回 0,因为不需要任何硬币就已经凑出目标金额了。
2、确定「状态」,也就是原问题和子问题中会变化的变量。由于硬币数量无限,硬币的面额也是题目给定的,只有目标金额会不断地向 base case 靠近,所以唯一的「状态」就是目标金额 amount
。
3、确定「选择」,也就是导致「状态」产生变化的行为。目标金额为什么变化呢,因为你在选择硬币,你每选择一枚硬币,就相当于减少了目标金额。所以说所有硬币的面值,就是你的「选择」。
4、明确 dp
函数/数组的定义。我们这里讲的是自顶向下的解法,所以会有一个递归的 dp
函数,一般来说函数的参数就是状态转移中会变化的量,也就是上面说到的「状态」;函数的返回值就是题目要求我们计算的量。就本题来说,状态只有一个,即「目标金额」,题目要求我们计算凑出目标金额所需的最少硬币数量。
所以我们可以这样定义 dp
函数:dp(n)
表示,输入一个目标金额 n
,返回凑出目标金额 n
所需的最少硬币数量。
消除重叠子问题:比如 amount = 11, coins = {1,2,5}
时画出递归树看看:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mugG0adC-1672582276795)(C:\Users\Yang\AppData\Roaming\Typora\typora-user-images\image-20221215184220356.png)]
发现我们多次计算了9,5。我们可以通过备忘录消除重叠子问题。我们可以通过定义一个memo数组,每次先去memo数组里查看是否曾经计算过,如果有,直接将值返回即可,减少了大量运算。
代码:
class Solution {
int memo[];
public int coinChange(int[] coins, int amount) {
memo = new int[amount+1];
// 备忘录初始化为一个不会被取到的特殊值,代表还未被计算
Arrays.fill(memo,-2);
return dp(coins,amount);
}
public int dp(int[] coins,int amount){
if(amount == 0)
return 0;
if(amount < 0){
return -1;
}
// 查备忘录,防止重复计算
if(memo[amount] != -2){
return memo[amount];
}
int res = Integer.MAX_VALUE;
for(int coin : coins){
// 计算子问题的结果
int resNum = dp(coins,amount-coin);
// 子问题无解则跳过
if(resNum == -1)
continue;
// 在子问题中选择最优解,然后加一
res = Math.min(res,resNum+1);
}
// 把计算结果存入备忘录
memo[amount] = (res == Integer.MAX_VALUE)?-1:res;
return memo[amount];
}
}
刚刚那种是自顶向下递归解法,还有一种方法就是dp数组迭代(自底向上)解法。
思路分析:
,关于「状态」「选择」和 base case 与之前没有区别,dp
数组的定义和刚才 dp
函数类似,也是把「状态」,也就是目标金额作为变量。不过 dp
函数体现在函数参数,而 dp
数组体现在数组索引:
dp
数组的定义:当目标金额为 i
时,至少需要 dp[i]
枚硬币凑出。
代码:
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
// 数组大小为 amount + 1,初始值也为 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], 1 + dp[i - coin]);
}
}
return (dp[amount] == amount + 1) ? -1 : dp[amount];
}
}
300.最长递增子序列(⭐⭐)
题目描述:
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
思路分析:
使用dp数组迭代(自底向上)解法,dp[i]代表前i个元素的最大递增子序列的长度,定义j的大小范围是0<j<i,在计算dp[i]时,我们已经知道了dp[j]的值,我们只要判断当前的nums[i]是否大于nums[j],若大于,则说明最长递增子序列的长度仍需要更新,此时将dp[j]+1借的到了dp[i]的值,我们在定义一个变量用于记录下最长递增子序列的长度并返回即可。
代码:
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums.length == 0){
return 0;
}
int[] dp = new int[nums.length];
//一个元素的最长递增子序列长度是1
dp[0] = 1;
int len = 1;
//将dp数组全部初始化为1
Arrays.fill(dp,1);
for(int i = 1;i < nums.length;i++){
//遍历0-j,如果nums[i] > nums[j],则更新dp[i];
for(int j = 0;j < i; j++){
if(nums[i] > nums[j]){
dp[i] = Math.max(dp[i],dp[j]+1);
}
}
//每计算出来一次dp[i],就更新一次此时最长递增子序列长度
len = Math.max(len,dp[i]);
}
return len;
}
}
53.最大子数组和(⭐⭐)
题目描述:
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。
思路分析:
定义dp[i]数组表示以nums[i]结尾的连续子数组的最大和。把dp[0]初始化为nums[0],因为如果nums数组只有一个元素的话,我们只需要把nums[0]返回即可。从头到尾遍历nums数组,假设数组 nums
的值全都严格大于 000,那么一定有 dp[i] = dp[i - 1] + nums[i]
。由于dp[i-1]有可能是负数,所以分类讨论:如果 dp[i - 1] > 0,那么可以把 nums[i] 直接接在 dp[i - 1] 表示的那个数组的后面,得到和更大的连续子数组;如果 dp[i - 1] <= 0,那么 nums[i] 加上前面的数 dp[i - 1] 以后值不会变大。于是 dp[i] 另起炉灶,此时单独的一个 nums[i] 的值,就是 dp[i]。最后在定义一个res值用于存储每次更新dp后最大子数组和的值并返回。
class Solution {
public int maxSubArray(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = nums[0];
int res = dp[0];
for(int i = 1;i < nums.length;i++){
if(dp[i-1] > 0){
dp[i] = dp[i-1] + nums[i];
}else{
dp[i] = nums[i];
}
res = Math.max(res,dp[i]);
}
return res;
}
}
55.跳跃游戏(⭐⭐)
题目描述:
给定一个非负整数数组 nums
,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
思路分析:
定义dp[i]为在i格能继续前行的格子数。初始化dp[0] = nums[0],从头到尾遍历nums,如果dp[i-1] == 0,说明没办法再继续往后走了,直接返回false,否则将计算走到当前格子还能继续往前走的格子数:dp[i] = Math.max(dp[i-1]-1,nums[i])。循环结束后,说明dp[i] 一直大于0,说明可以走到最后一个下标,返回true。
代码:
class Solution {
public boolean canJump(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = nums[0];
for(int i = 1;i < nums.length;i++){
if(dp[i-1] == 0){
return false;
}
dp[i] = Math.max(dp[i-1]-1,nums[i]);
}
return true;
}
}
198.打家劫舍(⭐⭐)
题目描述:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
思路分析:
首先考虑最简单的情况,如果只有一间房屋的话,则只能偷窃该房屋,如果有两件房屋的话,由于两间房子相邻,所以只能偷窃一家,只要选择两个房子中现金最多的屋子进行偷窃,就可以盗取到最高金额。现在考虑房屋数大于2的情况,对于k(k>2)间房屋有两种选项,第一种,偷窃第k间房屋,那么就不能偷窃第k-1间房屋,偷窃总额为前k-2间房屋的最高总金额与第k间房子的现金之和,第二种,不偷窃第k间,偷窃总金额为前k-1间房屋的最高总金额。用dp[i]表示前i间房屋最高偷窃金额,那么就有如下的状态方程:dp[i]=max(dp[i−2]+nums[i],dp[i−1])
代码:
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if(n == 1){
return nums[0];
}
if(n == 2){
return Math.max(nums[0],nums[1]);
}
int[] dp = new int[n];
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
for(int i = 2;i < n; i++) {
dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[n-1];
}
}
return Math.max(nums[0],nums[1]);
}
int[] dp = new int[n];
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
for(int i = 2;i < n; i++) {
dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[n-1];
}
}