完全背包问题 dp及记忆化搜索

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],那么我们每个体积状态都可以由这三种物品不断组合得到,我们的目的是得到每种背包剩余体积状态下我们哪种物品里选最优(每种之内必选一个,因为我们枚举的是背包体积状态,体积有变说明一定选了物品)。

由此,可以得到总的转移方程:

dp[v]=min(dp[v-vb[0]]+w[0],dp[v-vb[1]]+w[1],dp[v-vb[2]]+w[2])

这个总的转移方程无法一步实现,因为我们不知道我们有多少种物品,所以我们用循环分部实现

每次循环有:

dp[v]=min(dp[n-vb[i]],dp[v])

 解释,类似于打擂台取最值的思路,如果选当前种类的物品价值不如选别的(或不选)就值不变。反之更新最优值。

以上都在讨论最原始的完全背包问题,接下来是本题的思路(其实是一样的,换个皮罢了) 

边界讨论:主要就是当前总金额不能小于对应硬币面值时不能选。

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;
}

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值