Piggy-Bank (完全背包)+完全背包问题分析

文章介绍了如何使用动态规划解决完全背包问题,这是一种在限制条件下求解硬币组合最小面值的问题。通过分析完全背包与01背包的区别,给出了一维和二维数组的动态规划解决方案,并提供了具体的C++代码示例。此外,还讨论了如何优化时间复杂度并处理无法达成目标重量的情况。
摘要由CSDN通过智能技术生成

题目描述

有一个存钱罐,存储一些硬币。已知存钱罐的重量、存储了硬币后的存钱罐的重量以及每种硬币的重量和面值。求存钱罐中硬币的面值之和可能的最小值,或者如果无论如何放置硬币都不能达到存钱罐的重量要求,则输出“This is impossible.”

输入格式

第一行输入T表示有T组数据。每组数据首先一行输入E和F表示存钱罐没装硬币时和装了硬币后的重量。下一行输入N表示有N种硬币。接下来N行,每行输入两个整数P和W表示硬币的面值和重量。并且1<=N<=500,1<=P<=50000,1<=W<=10000,1<=E<=F<=10000

输出格式

对于每组数据输出一行,一行“The minimum amount of money in the piggy-bank is ans”,ans为该数据的解。或者解不存在输出“This is impossible”。

Sample Input

3
10 110
2
1 1
30 50
10 110
2
1 1
50 30
1 6
2
10 3
20 4

Sample Output

The minimum amount of money in the piggy-bank is 60.
The minimum amount of money in the piggy-bank is 100.
This is impossible.

题目分析

这道题是完全背包模板题。接下来先分析一下完全背包问题。

简单01背包中是从N个物品里选,每个物品只能用1次,完全背包则不同,每个物品可以用无限次。

所以01背包里每个物品只能是选(1)或者不选(0)两种状态,而完全背包里,假设背包体积为V,对于某个体积为v的物品可以有k+1种状态,其中满足k*v<=V,对应着选该物品0、1、2……k次。

 

01背包
如果物品能放入背包,则动态转移方程为 dp[i][j]=max( dp[i-1][j],dp[i-1][ j-v[i] ]+ w[i] ), j>=v[i]
如果物品不能放入背包,dp[i][j]=dp[i-1][j] ,j<v[i]

完全背包
动态转移方程满足:dp[i][j]=max{dp[i-1][ j-k×v[i] ] + k×w[i]},其中 0 ≤ k×v[i] ≤ j
可以发现,当k只能取0、1时的特例就是简单的0-1背包问题。

可以发现,选0次,即不选是肯定存在的,dp[i][j]=dp[i-1][j];
然后选1次,那就不一定存在了,可能可以选,也可能不可以选,假设可以选,在选0次该物品求出dp[i][j]的基础上,确定是否选该物品:if (j>=v[i]) dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);
选2次、选3次,依次类推,一直到选k次。

 上面这种也就是简单01背包中二维数组实现的写法,即手动模拟for循环,循环两次,不过完全背包中是循环k次,(k>1),但是对于只有两种选择的01背包,也可以用if、else判断,见滚动数组实现。

二维数组实现 

#include<bits/stdc++.h>
using namespace std;

const int maxn = 1010, maxv = 1010;
int v[maxn], w[maxn];
int dp[maxn][maxv];//N行V列,0行0列初始为0

int main() {
    int N, V;  //N为物品种类数,V为背包总容量
    cin >> N >> V;
    for (int i = 1; i <= N; i++) cin >> v[i] >> w[i];

    for (int i = 1; i <= N; i++)
        for (int j = 0; j <= V; j++) //"已经初始化第0列为0了,所以不管j从1还是从0开始,都一样"
            for (int k = 0; k * v[i] <= j; k++)
                dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v[i]] + k * w[i]);

    cout << dp[N][V] << endl;

    return 0;
}

上面第二重循环,j从0开始时,由于物品体积都大于0,继续循环的条件k*v[i]<=j,所以当j=0时,只会循环一次,即不选择物品时,dp[i][0]=dp[i-1][0],最终等于dp[0][0]=0,所以当初始化第0列以后,j从1开始就行。

二重循环里的语句三重循环的循环k次可以类比简单01背包的二重循环里面的语句,01背包是循环两次的简便写法,完全背包则是循环k次,不过为了dp[i][j]第一次能够取到dp[i-1][j],应该给dp[i][j]先初始化一个小于等于0的值,因为最后得到的dp二维表里,值最小值就是0。

时间复杂度优化

上面要经过三重循环,时间复杂度较高,我们可以将 f[i, j] = max { f[i-1, j-k*v[i]] + k*w[i] } 展开,就可以发现如下规律:

假设第i个物品体积为v,价值为w,最多能将k个该物品装进体积为j的背包,即k*v<=j

对于选择前i个物品,体积为j的背包:最多可以装入k个该物品,

f[i,j]=max(f[i−1][j],f[i−1][j−v]+w,f[i−1][j−2v]+2w……f[i−1][j−kv]+kw)

对于选择前i个物品,体积为j-v的背包:最多可以装入k-1个该物品,

二者进行等量代换,可以得到优化后的完全背包的状态转移方程f[i][j]=max(f[i-1][j],f[i][j-v]+w)
对比简单01背包的状态转移方程:f[i][j]=max(f[i-1][j],f[i-1][j-v]+w)

#include <iostream>
using namespace std;

const int maxn = 1010, maxv = 1010;
int v[maxn], w[maxn];
int dp[maxn][maxv];//N行V列,0行0列初始为0

int main() {
    int N, V;
    cin >> N >> V;
    for (int i = 1; i <= N; i++) cin >> v[i] >> w[i];

    for (int i = 1; i <= N; i++)
        for (int j = 0; j <= V; j++) {
            dp[i][j] = dp[i - 1][j];
            if (j >= v[i]) dp[i][j] = max(dp[i][j], dp[i][j - v[i]] + w[i]);
        }
    cout << dp[N][V] << endl;

    return 0;
}

 仅有一处不同,可以看出简单01背包的第i行的dp[i][j]完全取决于第i-1行的dp[i-1][0]到dp[i-1][j]的数据,
而完全背包第i行的dp[i][j]则取决于第i-1行的一个数据dp[i-1][j]以及第i行本身的dp[i][0]到dp[i][j-1]的数据。

优化+一维数组实现

完全背包中第i行前几列更新的数据需要被后几列用到,所以像简单背包那样进行一维数组优化的时候,二重循环就不能倒序循环了,要正序循环了。

可以理解为给体积为j的背包中选取k个某物品也是一个递推的过程,选取k个该物品就去选取k-1个该物品的时候找答案,一直往下找。

而01背包中若是也正序更新,第i-1行的数据就会被第i行新更新的数据覆盖,但后面用到的是第i-1行的数据,为了避免数据被破坏,所以倒序更新。

#include <iostream>
using namespace std;

const int maxn = 1010, maxv = 1010;
int v[maxn], w[maxn];
int dp[maxv];

int main() {
    int N, V;
    cin >> N >> V;
    for (int i = 1; i <= N; i++) cin >> v[i] >> w[i];

    for (int i = 1; i <= N; i++)
        for (int j = v[i]; j <= V; j++)
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);

   cout<<dp[V]<<endl;

    return 0;
}

本题Code:

#include<bits/stdc++.h>
using namespace std;
int dp[10005];
int main()
{
	int n, t, e, f, cost, value, v;
	cin >> t;
	while (t--)
	{
		cin >> e >> f;
		v = f - e;
		cin >> n;
		memset(dp, -1, sizeof(dp));  //初始化状态为-1,-1表示状态未到达
		dp[0] = 0;
		for (int i = 0; i < n; i++) {
			cin >> value >> cost;   //读取每种硬币的价值和重量
			for (int j = 0; j + cost <= v; j++) {
				if (dp[j] == -1) continue;  //状态未到达,不能进行转移
				else if (dp[j + cost] == -1) {  //如果第一次到达该状态,直接赋值
					dp[j + cost] = dp[j] + value;
				}
				else {
					dp[j + cost] = min(dp[j + cost], dp[j] + value);
				}
			}
		}
		if (dp[v] == -1) {  //dp[v]为-1说明所有硬币任意组合都不能得到v的重量
			cout << "This is impossible." << endl;
		}
		else {
			cout << "The minimum amount of money in the piggy-bank is " << dp[v] << "." << endl;
		}
	}

	return 0;
}

本文完全背包问题的分析大多来源于
原文链接:https://blog.csdn.net/HangHug_L/article/details/114238728

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值