题目来源
题目描述
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
}
};
题目解析
思路一
暴力递归
定义一个尝试函数: 考虑coins[idx…],组成rest的最少张数
int process(vector<int>& coins, int idx, int rest)
令N = coins.length(),那么,有如下可能:
(1)如果 idx == N,也就是没有硬币可以选择了,因此判断rest是否为0,如果为0,返回0张(因为0张硬币可以组成0元钱),否则返回无效
(2)否则,有硬币可以选择。因此尝试使用0张、1张、…张coins[idx]去组成rest
下面,我们用INT32_MAX标记无效
class Solution {
// coins[index...]面值,每种面值张数自由选择,组成idx需要多少张
int process(vector<int>& coins, int idx, int rest){
if(idx == coins.size()){ //已经没有面值能够考虑了
return rest == 0 ? 0 : INT32_MAX; //如果此时剩余的钱为0,返回0张
}
int ans = INT32_MAX; //最少张数,初始时为INT32_MAX,因为还没找到有效解
//依次尝试使用当前面值(arr[i])0张、1张、zhang张,但不能超过rest
for (int zhang = 0; zhang * coins[idx] <= rest; ++zhang) {
//使用了zhang张aim[i],剩下的钱为 rest - zhang*arr[i], 交给剩下的面值去搞定 (arr[i+1...N-1])
int next = process(coins, idx + 1, rest - zhang *coins[idx]);
if(next != INT32_MAX){ // 注意,**必须**next有效时,才去比较
ans = std::min(zhang + next, ans);
}
}
return ans;
}
public:
int coinChange(vector<int>& coins, int amount) {
int k = process(coins, 0, amount) ;
return k == INT32_MAX ? -1 : k ;
}
};
下面我们用-1标记无效:
int process(vector<int>& coins, int idx, int rest){
if(idx == coins.size()){
return rest == 0 ? 0 : -1;
}
int ans = -1;
for (int zhang = 0; zhang * coins[idx] <= rest; ++zhang) {
int next = process(coins, idx + 1, rest - zhang *coins[idx]);
if(next != -1){
ans = ans == -1 ? next + zhang : std::min(zhang + next, ans);
}
}
return ans;
}
注意两点:
- ans也必须用-1标记;
- ans必须先变成有效一次,然后才去和next + zhang比较
暴力递归改动态规划
用-1标记时:
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int N = coins.size();
std::vector<std::vector<int>> dp(N + 1, std::vector<int>(amount + 1));
dp[N][0] = 0;
for (int rest = 1; rest <= amount; ++rest) {
dp[N][rest] = -1;
}
for (int idx = N - 1; idx >= 0; --idx) {
for (int rest = 0; rest <= amount; ++rest) {
int ans = -1;
for (int zhang = 0; zhang * coins[idx] <= rest; ++zhang) {
int next = dp[idx + 1 ][rest - zhang *coins[idx]];
if(next != -1){
ans = ans == -1 ? next + zhang : std::min(zhang + next, ans);
}
}
dp[idx][rest] = ans;
}
}
return dp[0][amount];
}
};
用INT_MAX标记时:
int coinChange(vector<int>& coins, int amount) {
int N = coins.size();
std::vector<std::vector<int>> dp(N + 1, std::vector<int>(amount + 1, INT32_MAX));
dp[N][0] = 0;
for (int idx = N - 1; idx >= 0; --idx) {
for (int rest = 0; rest <= amount; ++rest) {
int ans = INT32_MAX;
for (int zhang = 0; zhang * coins[idx] <= rest; ++zhang) {
int next = dp[idx + 1 ][rest - zhang *coins[idx]];
if(next != INT32_MAX){
ans = std::min(zhang + next, dp[idx][rest]);
}
}
dp[idx][rest] = ans;
}
}
return dp[0][amount];
}
斜率优化
class Solution{
public:
int coinChange(vector<int>& coins, int amount) {
int N = coins.size();
std::vector<std::vector<int>> dp(N + 1, std::vector<int>(amount + 1, INT32_MAX));
dp[N][0] = 0;
for (int idx = N - 1; idx >= 0; --idx) {
for (int rest = 0; rest <= amount; ++rest) {
dp[idx][rest] = dp[idx + 1][rest];
if(rest - coins[idx] >= 0 && dp[idx][rest - coins[idx]] != INT32_MAX){
dp[idx][rest] = std::min(dp[idx][rest], dp[idx][rest - coins[idx]] + 1);
}
}
}
return dp[0][amount];
}
};
思路二:暴力尝试
class Solution {
int process(vector<int>& coins, int idx, int rest){
if(idx == coins.size()){
return rest == 0 ? 0 : INT32_MAX;
}
// 对于当前idx,有两种选择
int p1 = process(coins, idx + 1, rest); //不要
int p2 = INT32_MAX;
if(rest >= coins[idx]){ // 要:(必须满足条件才能要)
p2 = process(coins, idx, rest - coins[idx]);
if(p2 != INT32_MAX){
p2 = p2 + 1;
}
}
return std::min(p1, p2);
}
public:
int coinChange(vector<int>& coins, int amount) {
return process(coins, 0, amount);
}
};
动态规划:状态转移方程推导
为什么这道题不能用贪心解决?
- 为什么不能使用贪心算法思路来解决,因为使用贪心算法的前提是无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。 无后滞性,即去掉过去,不影响将来。
- 比如对于硬币[1, 2, 5],和target,贪心指的是尽量用最大的硬币,假设现在我们已经选取了{5, 5},此时剩下的target为1。如果我们拿掉5,那么剩下的target就变成了6,有后滞性
所以我们只能穷举所有的可能性。当然我们应该聪明的选择,也就是使用动态规划。
这道题类似于完全背包问题,
- 每个物品都可以无限使用,但是要求背包必须装满,而且要求背包中的物品数目最少
- (最值型的动态规划)
动态规划思路分析。
(1)第一步:也是最重要的一步。确定状态。
- 什么叫做确定状态呢?
- 简单来说,解动态规划的时候需要开辟一个数组,数组dp[i]或者dp[i][j]代表什么,i和j有分别代表什么
- 怎么确定状态呢?需要两个东西
- 最后一步
- 子问题
- 关于最后一步:
- 可以这样想:虽然我们不知道最优策略是什么,但是最终的方案肯定是K枚硬币 a 1 、 a 2 、 . . . . . . a k a_1、a_2、......a_k a1、a2、......ak面值加起来是 t a r g e t target target。
- 不管方案是什么,一定有一枚最后的硬币 a k a_k ak
- 除掉这枚硬币,前面的硬币的面值加起来是 t a r g e t − a k target- a_k target−ak
- 也就是说:
- 我们不关心前面的k-1枚硬币是怎么拼出 t a r g e t − a k target - a_k target−ak的,我们也不知道 a k a_k ak和 K K K,我们唯一可以确定的是前面的硬币拼出了 t a r g e t − a k target - a_k target−ak
- 因为是最优策略,所以拼出 t a r g e t − a k target - a_k target−ak的硬币数一定是最少的
- 关于子问题
- 从上面的问题,我们要求:最少用多少枚硬币可以拼出 t a r g e t − a k target - a_k target−ak。
- 即:
- 子问题:最少用多少枚硬币可以拼出 t a r g e t − a k target - a_k target−ak
- 原问题:最少用多少枚硬币可以拼出target
- 可以看出,我们已经把问题规模缩小了。
- 为了简化定义:f(k)表示最少用多少枚硬币拼出了k,k表示目标,f(k)返回多少枚硬币。可以看出,只有入参是不同的
- 还剩下一个问题:最后那枚硬币
a
k
a_k
ak是多少?(最优子结构)
- 最后的那枚硬币
a
k
a_k
ak只可能是
c
o
i
n
s
[
0
]
、
c
o
i
n
s
[
1
]
、
c
o
i
n
s
[
2
]
、
c
o
i
n
s
[
3
]
.
.
.
.
.
.
coins[0]、coins[1]、coins[2]、coins[3]......
coins[0]、coins[1]、coins[2]、coins[3]......c当中的某一个。
- 如果 a k a_k ak是coins[0], f ( t a r g e t ) f(target ) f(target)应该是 f ( t a r g e t − c o i n s [ 0 ] ) + 1 f(target -coins[0] ) + 1 f(target−coins[0])+1(假设最后这一枚硬币coins[0])
- 如果 a k a_k ak是coins[1], f ( t a r g e t ) f(target ) f(target)应该是 f ( t a r g e t − c o i n s [ 1 ] ) + 1 f(target -coins[1]) + 1 f(target−coins[1])+1(假设最后这一枚硬币coins[1])
- 如果 a k a_k ak是coins[3], f ( t a r g e t ) f(target ) f(target)应该是 f ( t a r g e t − c o i n s [ 3 ] ) + 1 f(target -coins[3]) + 1 f(target−coins[3])+1(假设最后这一枚硬币coins[3])
- …
- 也就是说,最后的话,有coins.size()种方案,我们要从这些种方案中选出最好的那个方案,即最少的硬币数,所以:
- f ( t a r g e t ) = m i n f ( t a r g e t − c o i n s [ 0 ] ) + 1 , f ( t a r g e t − c o i n s [ 1 ] ) + 1 , . . . . . . , f ( t a r g e t − c o i n s [ c o i n s . s i z e ( ) − 1 ] ) + 1 f(target) = min{f(target-coins[0] ) + 1, f(target-coins[1]) + 1, ......, f(target-coins[coins.size() - 1] ) + 1} f(target)=minf(target−coins[0])+1,f(target−coins[1])+1,......,f(target−coins[coins.size()−1])+1
- 可以看出,每一步尝试coins.size()种硬币,一共target步。与递归算法相比,没有任何重复运算,所以时间复杂度是target * coins.size()
- 最后的那枚硬币
a
k
a_k
ak只可能是
c
o
i
n
s
[
0
]
、
c
o
i
n
s
[
1
]
、
c
o
i
n
s
[
2
]
、
c
o
i
n
s
[
3
]
.
.
.
.
.
.
coins[0]、coins[1]、coins[2]、coins[3]......
coins[0]、coins[1]、coins[2]、coins[3]......c当中的某一个。
(2)第二步:转移方程
- 设状态f[X]=最少用多少枚硬币拼出X,X为目标,f[X]为最少的硬币数
- 对于任意X,它有coins.size()个最优子结构,它们之间的关系是:
- f ( t a r g e t ) = m i n f ( t a r g e t − c o i n s [ 0 ] ) + 1 , f ( t a r g e t − c o i n s [ 1 ] ) + 1 , . . . . . . , f ( t a r g e t − c o i n s [ c o i n s . s i z e ( ) − 1 ] ) + 1 f(target) = min{f(target-coins[0] ) + 1, f(target-coins[1]) + 1, ......, f(target-coins[coins.size() - 1] ) + 1} f(target)=minf(target−coins[0])+1,f(target−coins[1])+1,......,f(target−coins[coins.size()−1])+1
(3)第三步:初始条件和边界情况
- 对于
f
(
t
a
r
g
e
t
)
=
m
i
n
f
(
t
a
r
g
e
t
−
c
o
i
n
s
[
0
]
)
+
1
,
f
(
t
a
r
g
e
t
−
c
o
i
n
s
[
1
]
)
+
1
,
.
.
.
.
.
.
,
f
(
t
a
r
g
e
t
−
c
o
i
n
s
[
c
o
i
n
s
.
s
i
z
e
(
)
−
1
]
)
+
1
f(target) = min{f(target-coins[0] ) + 1, f(target-coins[1]) + 1, ......, f(target-coins[coins.size() - 1] ) + 1}
f(target)=minf(target−coins[0])+1,f(target−coins[1])+1,......,f(target−coins[coins.size()−1])+1,需要解决两个问题:
- 什么时候停下来?
- 也就是初始条件也是边界:F[0] = 0,当要拼出0元钱时只需要0枚硬币
- 如果f(target-coins[i])小于0怎么办?
- 小于0,也就是不能拼出时,如果不能拼出Y,就定义F[Y] = 正无穷,比如F[-1] = F[-2] = … = 正无穷(正无穷,是因为从公式中可以看出,是要求要求最小)
- 什么时候停下来?
(4)第四步:计算顺序
- 先初始条件F[0] = 0
- 然后计算F[1]、F[2]、F[3]…F[27]
动态规划有两种写法:
递归解法:
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
std::vector<int> dp(amount + 1);
dp[0] = 0;
for (int i = 1; i <= amount; ++i) {
dp[i] = INT_MAX;
for (int coin : coins) {
if(
i >= coin // 背包大小 >= 物品大小
&& dp[i - coin] != INT_MAX // [背包容量 - 当前物品大小] = 剩下的容量 --> 能拼出 i - coins[j]
&& dp[i - coin] + 1 < dp[i] // 拼出[剩下的容量]的硬币数
){
dp[i] = dp[i - coin] + 1;
}
}
}
return dp[amount] == INT_MAX ? -1 : dp[amount];
}
};