669 · 换硬币
给出不同面额的硬币以及一个总金额. 写一个方法来计算给出的总金额可以换取的最少的硬币数量. 如果已有硬币的任意组合均无法与总金额面额相等, 那么返回 -1
.
你可以假设每种硬币均有无数个
总金额不会超过10000
硬币的种类数不会超过500, 每种硬币的面额不会超过100
样例1
输入:
[1, 2, 5]
11
输出: 3
解释: 11 = 5 + 5 + 1
样例2
输入:
[2]
3
输出: -1
分析:
确定子问题:主问题,n的总金额用value[ ]面值的硬币最少需要的硬币数dp[n]。可以分成n-value[i](上一步用了该面值硬币)的总金额用value[ ]面值的硬币最少需要的硬币数dp[n-value[i]]和上一步没用该硬币两种情况。所以可以分出子问题dp[n-value[i]],在最后对两个子问题取小即可。
状态讨论:每个子问题的状态都是当前总金额 n 和兑换最少需要的硬币数dp[n];
状态转移方程:刚接触完全背包问题,我认为这里好好的分析一下状态转移方程是非常重要的,我学习的时候看到的数学推导说实话确实难以理解,这里我想用我自己的思路来解释。
我们可以这样想,完全背包与01背包不同的点主要就是完全背包里的物品可以无限制取,01背包因为物品数和背包体积这两个因数是有限的,所以推导状态的时候以这两个方面为界限递推,而完全背包的的物品数是无限的,但他的仍然有背包体积限制,所以我们可以从体积入手枚举递推每一种状态。而物品又如何考虑?
这样想,每种物品object[i]可以无限取,可以看作我们有无限个属性(体积,价值)相同的不同种物品,这样就又回到的01背包。但是物品的总数是无限的,肯定无法像01背包那样对每个物品都进行一次决策,递推完输出最终答案。所以,我们可以枚举体积(本题中指总金额)状态,并计算每种体积状态下,对对应物品决策的最优解。
设我们有 0-2号物品对应体积和价值为vb[i],w[i],那么我们每个体积状态都可以由这三种物品不断组合得到,我们的目的是得到每种背包剩余体积状态下我们哪种物品里选最优(每种之内必选一个,因为我们枚举的是背包体积状态,体积有变说明一定选了物品)。
由此,可以得到总的转移方程:
这个总的转移方程无法一步实现,因为我们不知道我们有多少种物品,所以我们用循环分部实现
每次循环有:
解释,类似于打擂台取最值的思路,如果选当前种类的物品价值不如选别的(或不选)就值不变。反之更新最优值。
以上都在讨论最原始的完全背包问题,接下来是本题的思路(其实是一样的,换个皮罢了)
边界讨论:主要就是当前总金额不能小于对应硬币面值时不能选。
int coinChange(vector<int>& value, int sum) {//value为面值,sum为要用sum的总金额换取最少硬币
//dp[sum]=min(dp[sum+arr[j]]+1,dp[sum]+1)
int mx = 9999999;//标记,且不应影响状态转移
int n = value.size();//得到数组长度
int dp[100000] = { 0 };//一维dp数组,下标表示每个子问题的总金额,对应值为最少硬币数
for (int j = 1; j <= sum; j++) {//枚举递推每一种状态
dp[j] = mx;//标记每个子问题,如果无解就保留标记
for (int i = 0; i < n; i++) {//枚举每一种面值的硬币
if (j - value[i] >= 0 )//边界处理,如果剩余总金额不足够换该面值硬币或者上一种状态无解
dp[j] = min(dp[j - value[i]] + 1, dp[j]);//状态转移
}
}
if (dp[sum] == mx) return -1;//最终主问题保留标记,表示无解
return dp[sum];
}
最后这里面有个很妙的点就是枚举总金额时的dp[j]=max,标记为正无穷。这个式子不仅标记了dp[j]使有解和无解区分,而且不影响状态转移,也让边界处理更轻松了。可以想想,如果本身无解(金额小于所有硬币),标记不动。递推的时候如果上一种情况的是无解的,那么本次也是无解。
完全背包的dfs写法(无优化)
有 NN 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10
#include <iostream>
//#include<fstream>
#include<ctime>
#include<math.h>
#include<string>
#include<vector>
#include<algorithm>
using namespace std;
int vb[10001] = { 0 }, w[100001] = { 0 };
int mx = 0;
int n, v;
void dfs(int vleft,int va) {
if (vleft < 0)return;
if(vleft >= 0){
mx = max(mx, va);
}
for (int j = 1; j <= n; j++) {
dfs(vleft - vb[j], va + w[j]);
}
}
int main()
{
cin >> n >> v;
for (int j = 1; j <= n; j++) {
cin >> vb[j] >> w[j];
}
dfs(v,0);
cout << mx;
return 0;
}
记忆化优化dfs后: (该搜索思路见01背包 记忆化搜索解法_m0_60777643的博客-CSDN博客)
这里简单提一下,该dfs到达最深处返回0,最后回到头节点的过程里不断返回0+w[i]+w[i-1]+...直到加到头节点的w[j],并每返回一个子节点的时候把此时的总价值填入该记忆(状态)数组中。最后到头节点停止。
而搜索的最深处也就是背包体积小到一个物品也带不走的情况(逻辑推断)。
#include <iostream>
#include<math.h>
#include<string>
#include<vector>
#include<algorithm>
#include<queue>
#include <iomanip>
using namespace std;
int object[1001] = { 0 };
int n, v;
int vb[10001] = { 0 }, w[10001] = { 0 };
int mx = 0;
int rec[10000] = { 0 };
int dfs(int vnow) {//dep为搜索深度,也就是当前有几个物品
//rec[vnow]已经初始化为0 ,
//也就是说dfs返回的时候价值沿路的价值为0+沿路的所选物品价值
if (rec[vnow] != 0)return rec[vnow];
for (int j = 0; j < n; j++) {//枚举每一种物品
if(vnow>=vb[j])
rec[vnow] = max(dfs(vnow - vb[j]) + w[j], rec[vnow]);
}
return rec[vnow];
}
int main() {
cin >> n >> v;
for (int j = 0; j < n; j++)
cin >> vb[j] >> w[j];
cout << dfs( v);
}
正常dp版本
//#include<graphics.h>
#include <iostream>
//#include<fstream>
#include<ctime>
#include<math.h>
#include<string>
#include<vector>
#include<algorithm>
using namespace std;
int dp[10000] = { 0 }, vb[10001] = { 0 }, w[100001] = { 0 };
int main()
{
int n, v;
cin >> n >> v;
for (int j = 1; j <= n; j++) {
cin >> vb[j] >>w[j];
}
for (int j = 1; j <= v; j++) {
for (int i = 1; i <=n; i++) {
if (j - vb[i] < 0)continue;
else
dp[j] = max(dp[j - vb[i]] + w[i], dp[j]);
}
}
cout << dp[v];
return 0;
}