换钱的最小货币数

换钱的最小货币数

一、题目

给定数组 arr,arr 中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数 aim 代表要找的钱数,求组成 aim 的最少货币数

举例:

1
2
3
4
5
6
arr[5, 2, 3], aim=20
4 张 5 元可以组成 20 元,其他的找钱方案都要使用更多张的货币,所以返回 4
arr[5,2,3], aim=0
不用任何货币就可以组成 0 元,返回 0
arr[3,5], aim=2
根本无法组成 2 元,钱不能找开的情况下默认返回 -1

二、思路

动态规划的思路。组成 aim 所需的最小货币数,可以展开成分别组成 0 - aim 所需的最小货币数。我们申请一个二维数组 dp。列代表 0 - aim,行代表给的钱的面值。

dp 的第一列,此时 aim 为 0,那也就是说无需货币,即可组成 0 元。所以第一列全部填充 0

dp 的第一行,此时只能选择一个面值,也就是 arr[0]。那么此面值能组成的总钱数为 arr[0], 2*arr[0] ... k*arr[0]。当 k*arr[0] 等于某个 aim 时,填充 dp[ 0 ][ k*arr[0] ]k。其他位置所代表的钱数一律找不开,所以一律设置为 正数的最大值。

dp 的其他位置 dp[i][j] 如何计算呢?j 代表目前的总钱数 aim,i 代表当前的面值 arr[i]。此时我们求 dp[i][j] ,有多种情况

  • 如果不使用 i 位置的钱面值,那么就取 dp[i-1][j]。表示总钱数 j 没有变的情况下,不使用 i 位置的钱面值,这个位置是我们的 dp[i][j] 最小值的一个条件。
  • 如果使用一张 i 位置的钱面值,那么就取 dp[i-1][j - arr[i]] + 1。我们使用一张 i 位置的钱面值,那么此时总钱数就要减去 arr[i],那么我们已经求得了 dp[i][j - arr[i]] 的位置的最小值,直接拿来用
  • 如果使用两张 i 位置的钱面值,那么就取 dp[i-1][j - 2*arr[i]] + 2。同上
  • 如果使用 K 张 i 位置的钱面值,那么就取 dp[i-1][j - k*arr[i]] + k。同上

加深一下思路,行代表给定的钱的面值,列代表当前要组成的总钱数。好的,那么:

1
2
3
4
5
6
7
8
9
10
11
dp[i][j] = min( dp[i-1][ j-k*arr[i] ] + k )  此时 k >= 0  转换如下
dp[i][j] = min(dp[i-1][j], dp[i-1][ j-k*arr[i] ] + k ) 此时 k >= 1
dp[i][j] = min(dp[i-1][j], dp[i-1][ j-(y+1)*arr[i] ] + y+1) 使用 y+1 替换 k,此时 y >= 0
dp[i][j] = min(dp[i-1][j], dp[i-1][ j - y*arr[i] - arr[i] ] + y+1) 此时 y >= 0

由于表达式 dp[i-1][ j - y*arr[i] - arr[i] ] + y (y >= 0),我们可以做出判断
min{ dp[i-1][ j - y*arr[i] - arr[i] ] + y } (y >= 0) 其中 y 表示使用了 y 张面值为 arr[i] 的钱。也就是说我们使用了 y 张面值为 arr[i] 的钱成功/不成功的让总钱数变为 aim。即有如下:
dp[i][ j - arr[i] ] = min{ dp[i-1][ j - y*arr[i] - arr[i] ] + y }, y >= 0

所以最终的表达式就变为了
dp[i][j] = min( dp[i-1][j], dp[i][ j-arr[i] ] + 1 )

注意,我们需要判断条件,上面表达式中也有不成功的让总数变为 aim 的情况。也就是 j - arr[i] < 0 的情况,就说明发生了越界。说明 arr[i] 太大了,用一张都会超过总钱数 j。如果不能成功让钱的总数变为想要的数字,那就填充 max 作为标志,最后再来判断 max 这个标志即可。

三、代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <vector>
#include <iostream>

class Solution {
public:
int min_coins(const std::vector<int>& arr, int aim) {
if (arr.empty() || aim < 0) {
return -1;
}
int n = arr.size();
int32_t max = INT32_MAX;
std::vector<std::vector<int>> dp(n, std::vector<int>(aim+1, 0));
for (int j = 1; j <= aim; ++j) {
dp[0][j] = max;
if (j - arr[0] >= 0 && dp[0][j-arr[0]] != max) {
dp[0][j] = dp[0][j-arr[0]] + 1;
}
}
int left = 0;
for (int i = 1; i < n; ++i) {
for (int j = 1; j <= aim; ++j) {
left = max;
if (j - arr[i] >= 0 && dp[i][j-arr[i]] != max) {
left = dp[i][j-arr[i]] + 1;
}
dp[i][j] = std::min(left, dp[i-1][j]);
}
}
return dp[n-1][aim] != max ? dp[n-1][aim] : -1;
}
};

int main() {
Solution s;
std::vector<int> arr{5, 2, 5, 2};
auto res = s.min_coins(arr, 10);
std::cout << res << std::endl;
return 0;
}

时间复杂度和空间复杂度都为 O(n * aim)