我们已经进入动态规划有一段时间了,相信有很多小伙伴像我一样,对动态规划的dp数组是如何从二维优化成一维的产生过很大的疑惑,本篇文章我们通过Leetcode的322.零钱兑换(Coin Change)这道题,一步一步分析二维数组的推导过程以及如何通过二维数组优化成一维数组,希望这篇文章能帮助到有同样困惑的小伙伴(。・ω・。)ノ
题目描述
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回-1
。
你可以认为每种硬币的数量是无限的。
示例 1:
ini
输入: coins =[1, 2, 5], amount =11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:
ini
输入: coins =[2], amount =3
输出: -1
示例 3:
输入: coins = [1], amount = 0 输出: 0
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 2^31- 1
0 <= amount <= 10^4
题目地址
解题思路
二维数组
我们拿示例 1做详细的详细步骤拆解的分析,看看二维dp数组是如何推出的,进一步又是如何优化成一位数组的
输入: coins =[1, 2, 5], amount =11
输出: 3
解释: 11 = 5 + 5 + 1
由于题目中求得是最小值,所以需要把数组初始化为最大值,但是因为题目的实际含义,我们可以省点儿事儿,就初始化为amount+1
即12
,如下图
当使用二维数组进行推导时,此时每个值(dp[i][j]
)代表在数组coins
的[0,i]
范围内取硬币(每个硬币不限数量)总值为j
时的最少硬币数。
然后对初始值进行初始化,先初始化第一行的数据,由于第一个硬币面值是1,总值为多少,就需要多少个,如下图:
再初始化第一列的数据,总值为0,无论硬币面值是多少,方法都是0,如下图进行初始化:
初始化完成后我们开始遍历:
当新增硬币面值>总值时:取上一行相同总值所需的最少硬币数(新的硬币无法加入)
当新增硬币面值<=总值时:取上一行相同总值所需的最少硬币数 (新的硬币无法加入)和总值减去新增硬币面值后得到的"总值"所需要的最少硬币数中的较小值
同理推出第二行后面的值
同理推出第三行
按照上述逻辑,具体代码如下:
java
class Solution {
public int coinChange(int[] coins, int amount) {
if (amount == 0) {
return 0;
}
// dp[i][j] 表示从[0,i]中任选硬币总和为j的最少硬币数为dp[i][j]
int[][] dp = new int[coins.length][amount + 1];
// 初始化
for (int i = 0; i < coins.length; i++) {
for (int j = 0; j < amount + 1; j++) {
dp[i][j] = amount + 1;
}
}
for (int j = 0; j < amount + 1; j++) {
if (j >= coins[0] && j % coins[0] == 0) {
dp[0][j] = j / coins[0];
}
}
for (int i = 0; i < coins.length; i++) {
dp[i][0] = 0;
}
for (int i = 1; i < coins.length; i++) {
for (int j = 1; j < amount + 1; j++) {
if (j < coins[i]) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - coins[i]] + 1);
}
}
}
return dp[coins.length - 1][amount] > amount ? -1 : dp[coins.length - 1][amount];
}
}
执行结果
复杂度分析
- 时间复杂度:O(n*amount),其中 amount是总金额,n 是数组 coins 的长度
- 空间复杂度:O(n*amount)
接下来,我们看看如何将二维数组优化为一位数组
一维数组
通过上面的分析我们可以看到整个推导顺序是从上至下 ,从左到右
并且总值的增长正好与序号的增长一致,因此我们可以使用一位数组的序号作为总值,一个一个硬币来进行最少硬笔数的计算,不断更新一维数组值,将二维数组压缩成一维数组。此时每个值(dp[j]
)代表在数组coins
的[0,i]
范围内取硬币(每个硬币不限数量)总值为j
时的最少硬币数。
当i=0
时,我们更新一维数组,如下图:
当i=1
时,我们更新一维数组,如下图:
当i=2
时,我们更新一维数组,如下图:
完整代码如下:
java
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);
dp[0] = 0;
for (int i = 0; i < coins.length; i++) {
for (int j = 1; j < amount + 1; j++) {
if (j >= coins[i]) {
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
执行结果
复杂度分析
- 时间复杂度:O(n*amount),其中 amount是总金额,n 是数组 coins 的长度
- 空间复杂度:O(amount)
总结
可以看到其实无论是使用一维数组还是二维数组,整体的逻辑以及时间复杂度并没有变化,只是在空间复杂度上进行了优化,而能进行优化的本质原因也是因为在本行的值只和它上一行的值以及当前行的值相关(详见上述推导图),其实就是数组的复用。