题目地址:
https://leetcode.com/problems/coin-change/
给定若干硬币的面值数组 A A A和一个非负整数 x x x,问 x x x可以至少由多少个硬币拼出。如果拼不出则返回 − 1 -1 −1。
很经典的一道题,可以用动态规划做。设 f ( x ) f(x) f(x)为 x x x至少需要多少枚硬币支付,设 c ⃗ \vec{c} c是硬币面额向量。可以这么想,如果我们能确定拼出 x x x的最优解有某个硬币 c i c_i ci,那么最优解删除掉一个硬币 c i c_i ci所得到的解,一定是拼出 x − c i x-c_i x−ci的最优解,否则的话我们就可以构造出一个更优解来拼出 x x x。所以我们需要做的,就是枚举最优解可能有的某个硬币。举个例子来说,假设我们有硬币 2 , 4 , 5 2,4,5 2,4,5要拼出 6 6 6,我们可以先假设最优解有一枚硬币 5 5 5,那么最优解删掉这面值为 5 5 5的硬币,一定就成为了拼出 6 − 5 = 1 6-5=1 6−5=1的最优解,然而拼出 1 1 1是不存在解的,所以也不存在含 5 5 5的最优解。接下来我们假设最优解有一枚硬币 4 4 4,那么最优解删掉这个 4 4 4一定得到了 6 − 4 = 2 6-4=2 6−4=2的最优解,而显然 f ( 2 ) = 1 f(2)=1 f(2)=1,所以我们就知道了,含硬币 4 4 4的最优解一定是一枚 4 4 4和一枚 2 2 2。然而这到底是不是全局最优解呢?不确定,因为我们还要试一下最优解含一枚 2 2 2的情况,我们发现 f ( 4 ) = 1 f(4)=1 f(4)=1,所以含 2 2 2的最优解一定是一枚 4 4 4和一枚 2 2 2,到此时,我们已经枚举完了最优解所有可能满足的情况,接着选一个全局最优的情况就行了,于是得到 f ( 6 ) = 2 f(6)=2 f(6)=2。
我们可以看出来,这题本质是个搜索的问题,搜索的出口,就是当最优解已经显然可以得到的时候。接着我们发现,要求一个规模较大的最优解,我们需要频繁去求规模更小的最优解。所以为了提高效率,我们可以空间换时间,用记忆化的方式来避免重复计算。具体来说,我们可以用一个数组来记录每次得到的最优解,这样求规模更大的最优解的时候,我们就可以直接去查表,而不是再计算一遍了。
数学的递推方程角度来说,如果用 ∞ \infty ∞代表无法支付,那么 f f f有递推方程: f ( x ) = { 1 , ∃ i , x = c i 1 + min { i : c i < x } f ( x − c i ) , ∃ i , x > c i ∞ , ∄ i , x > c i f(x)=\begin{cases} 1,\ \exists i,x=c_i\\ 1+\min_{\{i:c_i<x\}}f(x-c_i),\ \exists i,\ x>c_i\\ \infty,\ \nexists i,\ x>c_i\ \end{cases} f(x)=⎩ ⎨ ⎧1, ∃i,x=ci1+min{i:ci<x}f(x−ci), ∃i, x>ci∞, ∄i, x>ci 数学里可以用正无穷来表达无法支付,代码里却不能这么写。我们只需要用一个不可能取得到的值来标记“无法支付”就可以了。比如,我们可以用 x + 1 x+1 x+1来代表无解。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n = coins.size();
int f[n + 1][amount + 1];
fill(&f[0][0], &f[0][0] + (n + 1) * (amount + 1), amount + 1);
f[0][0] = 0;
for (int i = 1; i <= n; i++)
for (int j = 0; j <= amount; j++) {
f[i][j] = f[i - 1][j];
if (j >= coins[i - 1])
f[i][j] = min(f[i][j], f[i][j - coins[i - 1]] + 1);
}
return f[n][amount] == amount + 1 ? -1 : f[n][amount];
}
};
时空复杂度 O ( n m ) O(nm) O(nm), n n n为硬币面值种类数, m m m为要凑出的数字。