题目概述
题目链接:点我做题
题解
一、缓存法优化递归
不管怎么样先要想好这个题的算法,假设需要组成的面额为
S
S
S,组成面额
S
S
S需要的最小硬币数是
F
(
S
)
F(S)
F(S),假设一系列硬币的面额分别为
[
c
0
,
c
1
,
.
.
.
,
c
n
−
1
]
[c_0,c_1,...,c_{n-1}]
[c0,c1,...,cn−1],那么我们可以这样,组成面额
S
S
S的最小硬币数
F
(
S
)
F(S)
F(S)他不就等于取出一枚硬币后组成剩余面额的最小硬币数中的最小值加上1嘛,把这句话总结成状态转移方程:
F
(
S
)
=
m
i
n
0
<
i
<
n
(
F
(
S
−
c
i
)
)
+
1
F(S)=min_{0<i<n}(F(S-ci))+1
F(S)=min0<i<n(F(S−ci))+1
并且当S等于0时,表明原面额已经组成完毕,不再需要硬币,所以
F
(
0
)
=
9
F(0)=9
F(0)=9;
当S小于0时,表用使用上一轮的
c
i
c_i
ci使得组成的面额超过了待组成的面额
S
S
S,这种情况就是组成失败了,令
F
(
S
)
=
−
1
,
w
h
e
n
S
<
0
F(S)=-1,whenS<0
F(S)=−1,whenS<0,并且我们希望如果组成失败了就不要回到上一层再上+1了,直接去往上返回-1。
如果就这样硬暴力递归,时间会不过,因为纯暴力递归会在每一个地方一层一层的展开,而不是如果上次结算过了就直接返回上次的值,纯暴力递归有很多重复计算,因此我们可以考虑一个缓存的想法,用一个S大小的数组dp
记录计算过的
F
(
S
)
F(S)
F(S),初始时
S
T
L
STL
STL会帮我们自动全部初始化成0,进入递归后,先检查是否计算过这一层,即S>0 && dp[S]!=0
,如果计算过,那么就直接返回
d
p
[
S
]
dp[S]
dp[S],否则再进去判断。
代码:
class Solution {
//设F(S)代表组成金额S所需要的的最少硬币数
//那么显然F(S) = minF(S - coins[i]) + 1
//上面的是状态转移方程
//且S=0时,F(S)=0
//当S<0时,F(S)=-1表示这个状态组成不了金额S
//我们用dp[i]记录之前已经计算过的 这样递归到计算过的时候就不必展开了
public:
int coinChange(vector<int>& coins, int amount)
{
if (amount == 0)
{
return 0;
}
//dp数组来记录之前计算过的状态
vector<int> dp(amount + 1);
return F(amount, dp, coins);
}
int F(int restmoney, vector<int>& dp, const vector<int>& coins)
{
//先检查是否计算过
if (restmoney > 0 && dp[restmoney] != 0)
{
return dp[restmoney];
}
int ans = 0;
//对应F(0)=0
if (restmoney == 0)
{
ans = 0;
}
//如果剩余金额小于0了,说明组成面额失败 返回-1
else if (restmoney < 0)
{
ans = -1;
}
//走到这说明restmoney>0
else
{
//最小值先初始化设置成INT_MAX
int curmin = INT_MAX;
//通过循环遍历coins数组获得F(S-coins[i])的最小是
for (auto e : coins)
{
int curret = F(restmoney - e, dp, coins);
//没有返回-1说明能组成当前面额 才有贪心取最小值的必要
if (curret != -1 && curret < curmin)
{
curmin = curret;
}
}
//curmin不等于INT_MAX说明至少有一个coins[i]可以组成当前面额
//才有+1的必要
if (curmin != INT_MAX)
{
ans = curmin + 1;
}
//否则就是当前面额无法组成 网上一层层直接返回-1
else
{
ans = -1;
}
}
//dp[0]本来就是0 不必更新 记录值时要考虑到不要越界
if (restmoney > 0)
{
dp[restmoney] = ans;
}
return ans;
}
};
时间复杂度:每一层我们都遍历了n个
c
o
i
n
s
[
i
]
coins[i]
coins[i],并且由于我们记录了历史数据,所以不会说计算每个值都要完全展开,所以S的面值,最多计算S*n次,每次计算是
O
(
1
)
O(1)
O(1)的运算,所以时间复杂度为
O
(
S
n
)
O(Sn)
O(Sn).
空间复杂度:一个S数量级的数组储存历史数据,且递归最多展开
S
S
S层,空间复杂度为
O
(
S
)
O(S)
O(S).
二、动态规划
不难发现为了得到
d
p
[
S
]
dp[S]
dp[S]需要的所有变量
d
p
[
S
−
c
o
i
n
s
[
i
]
]
dp[S-coins[i]]
dp[S−coins[i]],如果从dp[1]开始算,我们都会提前计算过,所以可以用动态规划从1计算到dp[S],
d
p
[
0
]
dp[0]
dp[0]默认设置成0就好。
代码:
class Solution {
public:
int coinChange(vector<int>& coins, int amount)
{
if (amount == 0)
{
return 0;
}
//有了前面递归的思路,接下来我们用纯动态规划
vector<int> dp(amount + 1, INT_MAX);
//INT_MAX表示尚未计算过
dp[0] = 0;//组成0元的硬币数肯定是0
int sz = coins.size();
for (int i = 1; i <= amount; i++)
{
//这轮循环中计算dp[i]
int curmin = INT_MAX;
for (int j = 0; j < sz; j++)
{
//这轮来遍历的是coins数组的每个元素 即S-coins[i]那一步
if (i - coins[j] >= 0)
{
curmin = min(curmin, dp[i - coins[j]]);
}
}
if (curmin != INT_MAX)
{
dp[i] = curmin + 1;
}
//如果等于INT_MAX说明不能组成当前面值
//直接保留原INT_MAX不管就行,表示组成不了当前面额
}
return dp[amount] == INT_MAX ? -1 : dp[amount];
}
};
时间复杂度:循环S次,每次循环都遍历了一遍coins数组,时间复杂度显然为
O
(
S
n
)
O(Sn)
O(Sn)
空间复杂度:存储S个状态,空间复杂度为
O
(
S
)
O(S)
O(S)