问题描述:
零钱兑换 II:给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。假设每一种面额的硬币有无限个。
例子:输入amount = 5,coins = {1, 2, 5}; 输出4
动态规划求解
令
d
p
[
i
]
[
j
]
dp[ i ][ j ]
dp[i][j]使用前i种硬币兑换金额
j
j
j 的方法次数,数组coins[]表示所存取的硬币金额。当采用动态规划思想来解决本问题时,对于前
i
i
i种硬币兑换金额
j
j
j , 其可能是仅由前
i
−
1
i-1
i−1种硬币达到金额
j
j
j,不选取当前硬币;也可能是
i
−
1
i-1
i−1种硬币达到金额
j
−
c
o
i
n
s
[
i
−
1
]
j-coins[i-1]
j−coins[i−1],然后选取1枚第
i
i
i种硬币;也可能是
i
−
1
i-1
i−1种硬币达到金额
j
−
c
o
i
n
s
[
i
−
1
]
∗
2
j-coins[i-1]*2
j−coins[i−1]∗2,然后选取2枚第
i
i
i种硬币.等等,直至选取第i种硬币的硬币数k不满足
j
−
c
o
i
n
s
[
i
−
1
]
∗
k
>
=
0
j-coins[i-1]*k>=0
j−coins[i−1]∗k>=0。所以
d
p
[
i
]
[
j
]
dp[ i ][ j ]
dp[i][j]的计算方法可以表示为:
d
p
[
i
]
[
j
]
=
Σ
k
=
0
j
<
=
c
o
i
n
s
[
i
−
1
]
d
p
[
i
−
1
]
[
j
−
c
o
i
n
s
[
i
−
1
]
∗
k
]
dp[ i ][ j ] = \Sigma^{j<=coins[i-1]}_{k=0}dp[i-1][j-coins[i-1]*k]
dp[i][j]=Σk=0j<=coins[i−1]dp[i−1][j−coins[i−1]∗k]
如图所示,对于
d
p
[
2
]
[
4
]
dp[ 2 ][ 4]
dp[2][4],此时可以选取的硬币值为2,则
d
p
[
2
]
[
4
]
=
d
p
[
1
]
[
4
]
+
d
p
[
1
]
[
2
]
+
d
p
[
1
]
[
0
]
dp[ 2 ][ 4] = dp[1][4] + dp[1][2] + dp[1][0]
dp[2][4]=dp[1][4]+dp[1][2]+dp[1][0]。特别的当
i
=
0
,
j
=
0
i=0,j=0
i=0,j=0的时候,只有不拿取硬币一种情况
d
p
[
0
]
[
0
]
=
=
1
dp[ 0][ 0 ] == 1
dp[0][0]==1。当
i
=
0
,
j
!
=
0
i=0,j!=0
i=0,j!=0时,不可能不拿硬币而达到金额j,故
d
p
[
0
]
[
j
]
=
=
0
dp[ 0][ j] == 0
dp[0][j]==0。之后将
i
i
i递增,逐步求取每个
d
p
[
i
]
[
j
]
dp[ i ][ j ]
dp[i][j]的值,当求取完整个数组时,
d
p
[
c
o
i
n
s
.
n
u
m
]
[
a
m
o
u
n
t
]
dp[ coins.num ][amount ]
dp[coins.num][amount]即为硬币组合总数。
程序实现需要编写三层循环,分别对应硬币种类金额i金额,金额金额j金额,可以选取的硬币数k。需要注意的是硬币种类 i i i循环时从1开始,金额 j j j循环时从0开始,获取第 i i i种硬币的价值时的位 c o i n s [ i − 1 ] coins[i-1] coins[i−1]。运行结果为96 ms,47.8 MB。
class Solution {
public int change(int amount, int[] coins) {
int n = coins.length;
int[][] dp = new int[n + 1][amount + 1];
dp[0][0] = 1;
for (int i = 1; i <= n ; i++) {
for (int j = 0; j <= amount; j++) {
for (int k = 0; k <= j/coins[i-1]; k++) {
dp[i][j] += dp[i-1][j - k * coins[i-1]];
}
}
}
return dp[n][amount];
}
}
时间复杂度优化
考虑到:
d
p
[
i
]
[
j
]
=
Σ
k
=
0
j
<
=
c
o
i
n
s
[
i
−
1
]
d
p
[
i
−
1
]
[
j
−
c
o
i
n
s
[
i
−
1
]
∗
k
]
dp[ i ][ j ] = \Sigma^{j<=coins[i-1]}_{k=0}dp[i-1][j-coins[i-1]*k]
dp[i][j]=Σk=0j<=coins[i−1]dp[i−1][j−coins[i−1]∗k]
d
p
[
i
]
[
j
−
c
o
i
n
s
[
i
−
1
]
]
=
Σ
k
=
0
j
−
c
o
i
n
s
[
i
−
1
]
<
=
c
o
i
n
s
[
i
−
1
]
d
p
[
i
−
1
]
[
j
−
c
o
i
n
s
[
i
−
1
]
−
c
o
i
n
s
[
i
−
1
]
∗
k
]
dp[ i ] [j-coins[i-1] ] = \Sigma^{j-coins[i-1]<=coins[i-1]}_{k=0}dp[i-1][j-coins[i-1]-coins[i-1]*k]
dp[i][j−coins[i−1]]=Σk=0j−coins[i−1]<=coins[i−1]dp[i−1][j−coins[i−1]−coins[i−1]∗k]
则有
d p [ i ] [ j ] = d p [ i ] [ j − c o i n s [ i − 1 ] ] + d p [ i − 1 ] [ j ] dp[ i ][ j ] = dp[ i ][ j-coins[i-1]] + dp[i-1][j] dp[i][j]=dp[i][j−coins[i−1]]+dp[i−1][j]
所以按照之前的算法会有很多重复计算的过程, d p [ i ] [ j ] dp[ i ][ j ] dp[i][j]可以由 d p [ i ] [ j − c o i n s [ i − 1 ] ] dp[ i ][ j-coins[i-1]] dp[i][j−coins[i−1]]和 d p [ i − 1 ] [ j ] dp[ i -1][ j ] dp[i−1][j]直接推出,这样可以很好的避免重复的计算(对应减少硬币数 k k k的循环),从而减小时间复杂度。对应这里有 d p [ 2 ] [ 4 ] = d p [ 1 ] [ 4 ] + d p [ 1 ] [ 2 ] + d p [ 1 ] [ 0 ] dp[ 2 ][ 4] = dp[1][4] + dp[1][2] + dp[1][0] dp[2][4]=dp[1][4]+dp[1][2]+dp[1][0]。
程序实现需要编写两层循环,分别对应硬币种类 i i i,金额 j j j。运行结果为5 ms 47.9 MB。可以看到运行时间大大缩短。
class Solution {
public int change(int amount, int[] coins) {
int n = coins.length;
int[][] dp = new int[n + 1][amount + 1];
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= amount; j++) {
if(j >= coins[i-1]){
dp[i][j] = dp[i-1][j] + dp[i][j - coins[i-1]];
}else{
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][amount];
}
}
空间复杂度优化
考虑到
d
p
[
i
]
[
j
]
dp[ i ][ j ]
dp[i][j]的值仅与第
i
−
1
i-1
i−1行有关系,可以采用滚动数组的思想来降低空闲复杂度。具体的,只维护一维的
d
p
dp
dp数组,其状态更新关系为:
d
p
[
j
]
=
d
p
[
j
−
c
o
i
n
s
[
i
−
1
]
]
+
d
p
[
j
]
dp[ j ] = dp[ j-coins[i-1]] + dp[j]
dp[j]=dp[j−coins[i−1]]+dp[j]
程序实现需要编写两层循环,分别对应硬币数i,金额j。运行结果为4 ms 38.8 MB,可以看到空间使用情况有所降低。
class Solution {
public int change(int amount, int[] coins) {
int n = coins.length;
int[] dp = new int[amount + 1];
dp[0]= 1;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= amount; j++) {
if(j >= coins[i-1]){
dp[j] = dp[j] + dp[j - coins[i-1]];
}else{
dp[j] = dp[j];
}
}
}
return dp[amount];
}
}