一个背包总容量为V,现在有N个物品,第i个 物品体积为weight[i],价值为value[i],现在往背包里面装东西,怎么装能使背包的内物品价值最大?
一个背包总容量为V,现在有N个物品,第i个 物品体积为weight[i],价值为value[i],每个物品都有无限多件,现在往背包里面装东西,怎么装能使背包的内物品价值最大?
给予不同面值的硬币若干种种(每种硬币个数无限多),如何用若干种硬币组合为某种面额的钱,使硬币的的个数最少?
在现实生活中,我们往往使用的是贪心算法,比如找零时需要13元,我们先找10元,再找2元,再找1元。如果我们的零钱可用的有1、2、5、9、10。我们找零18元时,贪心算法的策略是:10+5+2+1,四种,但是明明可以用两个9元的啊。这种问题一般使用动态规划来解决。
一、首先来看01背包问题
用一个数组f[i][j]表示,在只有i个物品,容量为j的情况下背包问题的最优解。第i个物品可以选择放进背包或者不放进背包(这也就是0和1),假设放进背包(前提是放得下),那么f[i][j]=f[i-1][j-weight[i]+value[i];如果不放进背包,那么f[i][j]=f[i-1][j]。
这就得出了状态转移方程:
f[i][j]=max(f[i-1][j],f[i-1][j-weight[i]+value[i])
实现代码
#include
using namespace std;
#define V 1500
unsigned int f[10][V];//全局变量,自动初始化为0
unsigned int weight[10];
unsigned int value[10];
#define max(x,y) (x)>(y)?(x):(y)
int main()
{
int N,M;
cin>>N;//物品个数
cin>>M;//背包容量
for (int i=1;i<=N; i++)
{
cin>>weight[i]>>value[i];
}
for (int i=1; i<=N; i++)
for (int j=1; j<=M; j++)
{
if (weight[i]<=j)
{
f[i][j]=max(f[i-1][j],f[i-1][j-weight[i]]+value[i]);
}
else
f[i][j]=f[i-1][j];
}
cout<
在hihocoder上面还讲到可以进一步优化内存使用。上面计算f[i][j]可以看出,在计算f[i][j]时只使用了f[i-1][0……j],i-1没有变化,j是从0一直递增,因此可以用一个一维数组存储i-1时求得j对应的每个f[j];然后求i时,利用i-1时的数组f[j]递推求得到i时f[j],数组复用同一个。再进一步思考,为了复用数组时不对数据产生污染,计算f[j]时应该从后往前算,即 j=M......1
for i=1……N
for j=M……1
f[j]=max(f[j],f[j-weight[i]+value[i])
实现代码:
#include
using namespace std;
#define V 1500
unsigned int f[V];//全局变量,自动初始化为0
unsigned int weight[10];
unsigned int value[10];
#define max(x,y) (x)>(y)?(x):(y)
int main()
{
int N,M;
cin>>N;//物品个数
cin>>M;//背包容量
for (int i=1;i<=N; i++)
{
cin>>weight[i]>>value[i];
}
for (int i=1; i<=N; i++)
for (int j=M; j>=1; j--)
{
if (weight[i]<=j)
{
f[j]=max(f[j],f[j-weight[i]]+value[i]);
}
}
cout<
解法1:
(1)对于完全背包问题状态转移方程:
f[i][j]=max(f[i-1][j-k*weight[i]+k*value[i]),其中0<=k<=j/weight[i]
可以理解为:j为背包可以容纳的重量,有i种物品时,向背包里添加第i种物品,第i种物品可以添加的个数范围是 0<=k<=j/weight[i]
(2)对于硬币找零问题状态转移方程:
f[i][j]=min(f[i-1][j-k*value[i]+k),其中0<=k<=j/value[i]
可以理解为:j为需要找零多少元,有i种硬币,找零时选取第i种硬币,第i种硬币可以选取的枚数是 0<=k<=j/value[i]
注意:其实上面这种解法看来貌似没什么问题,但是上面的递归公式有冗余计算,例如下面两个式子:
f[i][j]=max{ f[i−1,j− value(i)∗k] + value(i)∗k }, 0≤ k ≤ x/need(i)
f[i][j−value(i)]=max{ f[i−1][j− value(i)∗k] + value(i)∗k }, 1≤ k ≤ x/value(i)
在计算上面第一个式子时,又把第二个式子中大部分重新计算了一遍。 所以解法1并不高效,一般不会用。
解法2:
(1)对于完全背包问题状态转移方程:
f (j) = max{ f(j - weight[i]) + value[i], i = 0......N }
可以理解为:j为背包可以容纳的重量,有N种物品,
对于每种物品假设至少包含一个,至于到底包含多少个我们并不关心。
(2)对于最少硬币找零问题状态转移方程:
f (j) = min{ f(j - coin[i]) + 1, i = 0......N }
可以理解为:j为需要找零多少元,有N种硬币,对于每种硬币,我们可以依次假设f(i)中至少包含一个coin[j] (j=0, 1......N) ,然后得到所需的最少硬币是f(j- coin[i]) + 1,最后再从这N次假设中选出最小的就是f(i)。
有人可能会有疑问,为什么只是假设存在一块硬币coin[j],存在k块硬币难道不用考虑吗?假如f(i)真的包含多个coin[j],我们只取一个coin[j],那么剩下的几个coin[j]的最优组合肯定已经包含在 f(i - coin[j]) 里面了,我们根本不用关心它们。
解法3: 上面的状态转移方程比较难以理解(不常用),下面换一种更通用的状态方程
(1)对于完全背包问题状态转移方程:
f[ i ] [ j ] = max( f[i-1][j], f[ i ][ j- weight[i] ] + value[i] ) ,注意后面是f[i, j-weight[i]],i 没有减1
可以理解为:j为背包可以容纳的重量,有i种物品时,对于第i种物品,要么取或者不取,
至于取多少个我们并不关心。
(2)对于硬币找零问题状态转移方程:
f[i][j]=min( f[i-1][ j ], f [i ] [ j - value[i] ] + 1) ,注意后面是f[i, j-value[i]],i 没有减1
可以理解为:j为需要找零多少元,有i种硬币,找零时对于第i种硬币,
我们只考虑取或者不取,至于取多少个我们并不关心!
两种边界情况说明一下:
(1)f[0][j]=Integer.MAXVALUE ,因为 对金额为 j 的钱找零,但是可以的硬币面值种类为0,这显然是无法做到的。其实这是一个”未定义“的状态。它之所以初始为Integer.MAXVALUE
(2)f[i][0]=0,因为,对金额为0的钱找零,可用来找零的硬币种类有 i 种,金额为0怎么找啊,故设置为0。
/*
*
* @param coinsValues 可用来找零的硬币 coinsValues.length是硬币的种类
* @param n 待找的零钱
* @return 最少硬币数目
*/
public static int charge(int[] coinsValues, int n){
int[][] c = new int[coinsValues.length + 1][n + 1];
// 初始化边界条件
for(int i = 0; i <= coinsValues.length; i++) {
c[i][0] = 0;
}
for(int i = 0; i <= n; i++){
c[0][i] = Integer.MAX_VALUE;
}
for(int i = 1; i<=coinsValues.length; i++){ //i表示参加找零的硬币的种类1~i种硬币
for(int j = 1; j <= n; j++){//j表示需要找零的钱数
if(j < coinsValues[i-1]){
c[i][j] = c[i - 1][j];
continue;
}
//每个问题的选择数目---选其中较小的
if(c[i - 1][j] < (c[i][j - coinsValues[i-1]] +1)) {
c[i][j] = c[i - 1][j];
} else {
c[i][j] = c[i][j - coinsValues[i-1]] +1;
}
}
}
return c[coinsValues.length][n];
}