leetcode 322. 零钱兑换----完全背包套路解法详细再探
Posted 大忽悠爱忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了leetcode 322. 零钱兑换----完全背包套路解法详细再探相关的知识,希望对你有一定的参考价值。
零钱兑换完全背包套路解法再探
引言
leetcode 322. 零钱兑换本篇文章题解之前已经发过,但是对完全背包的解法只是模棱解释一番,今天再写一篇文章来详细探讨一下本题套用完全背包公式的解法
完全背包套路题目:
leetcode 279. 完全平方数
LintCode 440 · 背包问题 III—完全背包问题
本人目前刷题数量较少,先列举两道,O(∩_∩)O哈哈~
完全背包(朴素解法)
- 硬币相当于我们的物品,每种硬币可以选择「无限次」,我们应该很自然的想到「完全背包」。
- 如果不能,那么从现在开始就要培养这样的习惯:
- 当看到题目是给定一些「物品」,让我们从中进行选择,以达到「最大价值」或者「特定价值」时,我们应该联想到「背包问题」。
- 这本质上其实是一个组合问题:被选物品之间不需要满足特定关系,只需要选择物品,以达到「全局最优」或者「特定状态」即可。
- 再根据物品的「
选择次数限制
」来判断是何种背包问题。 - 本题每种硬币可以被选择「无限次」,我们可以直接套用「完全背包」的状态定义进行微调:
定义 dp[i][j] 为考虑前 i 件物品,凑成总和为 j 所需要的最少硬币数量。
- 为了方便初始化,我们一般让 dp[0][x] 代表不考虑任何物品的情况。
- 因此我们有显而易见的初始化条件:dp[0][0]=0,其余dp[0][x]=INF 。
- 代表当没有任何硬币的时候,存在凑成总和为 0 的方案,方案所使用的硬币为 0;凑成其他总和的方案不存在。
- 由于我们要求的是「最少」硬币数量,因此我们不希望「无效值」参与转移,可设 INF=INT_MAX。
- 当「状态定义」与「基本初始化」有了之后,我们不失一般性的考虑 dp[i][j] 该如何转移。
- 对于第 i 个硬币我们有两种决策方案:
- 不使用该硬币:dp[i][j]=dp[i-1][j]
- 使用该硬币,由于每种硬币可以被选择多次(容量允许的情况下),因此最优解应当是所有方案中的最小值。即dp[i][j]=min(dp[i-1][j-k*coin]+ k)
代码:
class Solution {
public:
int coinChange(vector<int>& coins, int amount)
{
int size = coins.size();//获取硬币个数
//因为有个初始状态0,即什么硬币都不考虑,因此行数加一行
vector<vector<int>> dp(size + 1, vector<int>(amount + 1, 0));
// 初始化(没有任何硬币的情况):只有 f[0][0] = 0;其余情况均为无效值。
// 这是由「状态定义」决定的,当不考虑任何硬币的时候,只能凑出总和为 0 的方案,所使用的硬币数量为 0
for (int i = 1; i <= amount; i++) dp[0][i] = INT_MAX;
// 有硬币的情况
for (int i = 1; i < size+1; i++)
{
int val = coins[i-1];//获取当前硬币的大小
for (int j = 0; j < amount + 1; j++)
{
//不考虑当前硬币的情况
dp[i][j] = dp[i - 1][j];
// 考虑当前硬币的情况(可选当前硬币个数基于当前容量大小)
for (int k = 1; k * val <=j; k++)
{
if (dp[i - 1][j - val * k] != INT_MAX)
dp[i][j] = min(dp[i][j], dp[i - 1][j - k * val] + k);
}
}
}
return dp[size][amount]==INT_MAX?-1: dp[size][amount];
}
};
无效状态的定义问题–顺带滚动数组优化
借这个问题,刚好说一下,我们初始化时,对于无效状态应该如何定义。
- 可以看到上述解法,将 INF 定义为 INT_MAX。
- 这是因为我们转移时取的是较小值,我们希望无效值不要被转移,所以将 INF 定义为较大的数,以代表数学上的 +00(正无穷)。
- 这很合理,但是我们需要注意,如果我们在 INF 的基础上进行累加的话,常规的语言会将其变成负数最小值。
- 也就是在正无穷基础上进行累加,会丢失其正无穷的含义,这与数学上的正无穷概念冲突。
- 因此,我们才有先判断再使用的习惯:
if (f[i-1][j] != INF) {
f[i][j] = Math.min(f[i][j], f[i-1][j]);
}
- 但事实上,如果每次使用都需要有前置检查的话,是很麻烦的。
- 于是我们有另外一个技巧,不直接使用 INT_MAX 作为 INF,而是使用一个比 INT_MAX 小的较大数来代表 INF。
- 相当于预留了一些「累加空间」给 INF。
- 比如使用 0x3f3f3f3f 作为最大值,这样我们使用 INF 做状态转移的时候,就不需要先判断再使用了。
代码:
class Solution {
public:
int coinChange(vector<int>& coins, int amount)
{
int INF = 0x3f3f3f3f;
int size = coins.size();//获取硬币个数
//因为有个初始状态0,即什么硬币都不考虑,因此行数加一行
vector<vector<int>> dp(2, vector<int>(amount + 1, 0));
// 初始化(没有任何硬币的情况):只有 f[0][0] = 0;其余情况均为无效值。
// 这是由「状态定义」决定的,当不考虑任何硬币的时候,只能凑出总和为 0 的方案,所使用的硬币数量为 0
for (int i = 1; i <= amount; i++) dp[0][i] = INF;
// 有硬币的情况
for (int i = 1; i < size + 1; i++)
{
int val = coins[i - 1];//获取当前硬币的大小
for (int j = 0; j < amount + 1; j++)
{
//不考虑当前硬币的情况
dp[i & 1][j] = dp[(i - 1) & 1][j];
// 考虑当前硬币的情况(可选当前硬币个数基于当前容量大小)
for (int k = 1; k * val <= j; k++)
{
dp[i & 1][j] = min(dp[i & 1][j], dp[(i - 1) & 1][j - k * val] + k);
}
}
}
return dp[size & 1][amount] == INF ? -1 : dp[size & 1][amount];
}
};
完全背包(一维优化)
-
显然朴素版的完全背包进行求解复杂度有点高。
-
之前我们从最朴素背包转移方程出发,从数学的角度去推导一维优化是如何来的。
-
这十分科学,而绝对严谨。
-
但每次都这样推导是十分耗时的。
-
因此,我们这次站在一个「更高」的角度去看「完全背包」问题。
-
我们知道传统的「完全背包」二维状态转移方程是:
-
经过一维空间优化后的状态转移方程是(同时容量维度遍历顺序为「从小到大」):
-
然后我们只需要对「成本」&「价值」进行抽象,并结合「换元法」即可得到任意背包问题的一维优化状态转移方程。
-
拿我们本题的状态转移方程来分析,本题的朴素状态转移方程为:
-
我们将硬币的面值抽象为「成本」,硬币的数量抽象「价值」,再对物品维度进行消除,即可得:
如果还不理解,可以将上述四个状态转移方程「两两成对」结合来看。
代码:
class Solution {
public:
int coinChange(vector<int>& coins, int amount)
{
int INF = 0x3f3f3f3f;
int size = coins.size();
vector<int> dp(amount + 1, 0);
for (int i = 1; i <= amount; i++) dp[i] = INF;
for (int i = 1; i < size + 1; i++)
{
int val = coins[i - 1];
//注意这里j的起点是val,比当前硬币面值val小的所需硬币总和,也不会去选,因此属于当前硬币不选择状态,那么数据维持上一次就可以,不用更新
for (int j = val; j < amount + 1; j++)
dp[j] = min(dp[j], dp[j - val] + 1);
}
return dp[amount] == INF ? -1 : dp[amount];
}
};
以上是关于leetcode 322. 零钱兑换----完全背包套路解法详细再探的主要内容,如果未能解决你的问题,请参考以下文章
LeetCode 446. 等差数列划分 II - 子序列(动态规划)/ 416. 分割等和子集 (背包问题)/322. 零钱兑换(完全背包)