@TOC
前言
代码随想录算法训练营day45
一、Leetcode 70. 爬楼梯 (进阶)
1.题目
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入:n = 2 输出:2 解释:有两种方法可以爬到楼顶。 1. 1 阶 + 1 阶 2. 2 阶
示例 2:
输入:n = 3 输出:3 解释:有三种方法可以爬到楼顶。 1. 1 阶 + 1 阶 + 1 阶 2. 1 阶 + 2 阶 3. 2 阶 + 1 阶
提示:
1 <= n <= 45
来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/climbing-stairs
2.解题思路
方法一:动态规划
思路和算法
我们用 f(x)f(x) 表示爬到第 xx 级台阶的方案数,考虑最后一步可能跨了一级台阶,也可能跨了两级台阶,所以我们可以列出如下式子:
f(x)=f(x−1)+f(x−2)f(x)=f(x−1)+f(x−2)
它意味着爬到第 xx 级台阶的方案数是爬到第 x−1x−1 级台阶的方案数和爬到第 x−2x−2 级台阶的方案数的和。很好理解,因为每次只能爬 11 级或 22 级,所以 f(x)f(x) 只能从 f(x−1)f(x−1) 和 f(x−2)f(x−2) 转移过来,而这里要统计方案总数,我们就需要对这两项的贡献求和。
以上是动态规划的转移方程,下面我们来讨论边界条件。我们是从第 00 级开始爬的,所以从第 00 级爬到第 00 级我们可以看作只有一种方案,即 f(0)=1f(0)=1;从第 00 级到第 11 级也只有一种方案,即爬一级,f(1)=1f(1)=1。这两个作为边界条件就可以继续向后推导出第 nn 级的正确结果。我们不妨写几项来验证一下,根据转移方程得到 f(2)=2f(2)=2,f(3)=3f(3)=3,f(4)=5f(4)=5,……,我们把这些情况都枚举出来,发现计算的结果是正确的。
我们不难通过转移方程和边界条件给出一个时间复杂度和空间复杂度都是 O(n)O(n) 的实现,但是由于这里的 f(x)f(x) 只和 f(x−1)f(x−1) 与 f(x−2)f(x−2) 有关,所以我们可以用「滚动数组思想」把空间复杂度优化成 O(1)O(1)。下面的代码中给出的就是这种实现。
3.代码实现
```java class Solution { public int climbStairs(int n) { int p = 0, q = 0, r = 1; for (int i = 1; i <= n; ++i) { p = q; q = r; r = p + q; } return r; } }
```
二、Leetcode 322. 零钱兑换
1.题目
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11 输出:3 解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3 输出:-1
示例 3:
输入:coins = [1], amount = 0 输出:0
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 231 - 1
0 <= amount <= 104
来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/coin-change
2.解题思路
方法一:记忆化搜索
我们能改进上面的指数时间复杂度的解吗?当然可以,利用动态规划,我们可以在多项式的时间范围内求解。首先,我们定义:
F(S)F(S):组成金额 SS 所需的最少硬币数量
[c0…cn−1][c0…cn−1] :可选的 nn 枚硬币面额值
我们注意到这个问题有一个最优的子结构性质,这是解决动态规划问题的关键。最优解可以从其子问题的最优解构造出来。如何将问题分解成子问题?假设我们知道 F(S)F(S),即组成金额 SS 最少的硬币数,最后一枚硬币的面值是 CC。那么由于问题的最优子结构,转移方程应为:
F(S)=F(S−C)+1F(S)=F(S−C)+1
但我们不知道最后一枚硬币的面值是多少,所以我们需要枚举每个硬币面额值 c0,c1,c2…cn−1c0,c1,c2…cn−1 并选择其中的最小值。下列递推关系成立:
F(S)=mini=0...n−1F(S−ci)+1 subject to S−ci≥0F(S)=i=0...n−1minF(S−ci)+1 subject to S−ci≥0
F(S)=0 ,when S=0F(S)=0 ,when S=0
F(S)=−1 ,when n=0F(S)=−1 ,when n=0
在上面的递归树中,我们可以看到许多子问题被多次计算。例如,F(1)F(1) 被计算了 1313 次。为了避免重复的计算,我们将每个子问题的答案存在一个数组中进行记忆化,如果下次还要计算这个问题的值直接从数组中取出返回即可,这样能保证每个子问题最多只被计算一次。
3.代码实现
```java public class Solution { public int coinChange(int[] coins, int amount) { if (amount < 1) { return 0; } return coinChange(coins, amount, new int[amount]); }
private int coinChange(int[] coins, int rem, int[] count) {
if (rem < 0) {
return -1;
}
if (rem == 0) {
return 0;
}
if (count[rem - 1] != 0) {
return count[rem - 1];
}
int min = Integer.MAX_VALUE;
for (int coin : coins) {
int res = coinChange(coins, rem - coin, count);
if (res >= 0 && res < min) {
min = 1 + res;
}
}
count[rem - 1] = (min == Integer.MAX_VALUE) ? -1 : min;
return count[rem - 1];
}
}
```
三、Leetcode 279.完全平方数
1.题目
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12 输出:3 解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13 输出:2 解释:13 = 4 + 9
提示:
1 <= n <= 104
来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/perfect-squares
2.解题思路
方法一:动态规划
思路及算法
我们可以依据题目的要求写出状态表达式:f[i]f[i] 表示最少需要多少个数的平方来表示整数 ii。
这些数必然落在区间 [1,n][1,n
]。我们可以枚举这些数,假设当前枚举到 jj,那么我们还需要取若干数的平方,构成 i−j2i−j2。此时我们发现该子问题和原问题类似,只是规模变小了。这符合了动态规划的要求,于是我们可以写出状态转移方程。
f[i]=1+minj=1⌊i⌋f[i−j2]f[i]=1+j=1min⌊i
⌋f[i−j2]
其中 f[0]=0f[0]=0 为边界条件,实际上我们无法表示数字 00,只是为了保证状态转移过程中遇到 jj 恰为 ii
的情况合法。
同时因为计算 f[i]f[i] 时所需要用到的状态仅有 f[i−j2]f[i−j2],必然小于 ii,因此我们只需要从小到大地枚举 ii 来计算 f[i]f[i] 即可。
3.代码实现
```java class Solution { public int numSquares(int n) { int[] f = new int[n + 1]; for (int i = 1; i <= n; i++) { int minn = Integer.MAX_VALUE; for (int j = 1; j * j <= i; j++) { minn = Math.min(minn, f[i - j * j]); } f[i] = minn + 1; } return f[n]; } }
```