题目一:01背包问题
一个背包总容量为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<iostream>
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<<f[N][M]<<endl;//输出最优解
}
在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<iostream>
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<<f[M]<<endl;//输出最优解
}
二、完全背包问题 和 硬币找零问题
其实这个两个问题非常相似,都是物品数目无限多,一个是不超过某个重量值W求最大value,一个是要获得某个value,求最小重量(每个硬币可以看成是重量为1的物品)。
解法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]) 里面了,我们根本不用关心它们。
[cpp]
view plain
copy
- #include<iostream>
- using namespace std;
- //money需要找零的钱
- //coin可用的硬币
- //硬币种类
- void FindMin(int money,int *coin, int n)
- {
- int *f =new int[money+1]();//存储1…money找零最少需要的硬币的个数
- int *coinValue=new int[money+1]();//最后加入的硬币,方便后面输出是哪几个硬币
- coinNum[0]=0;
- for(int i=1; i<=money; i++)
- {
- int minNum=i;//表示i个1元硬币正好找零i元,这是一种找零的组合,并不是所有组合里面最少的,下面寻找最少值
- int curMoney=0;//这次找零,在原来的基础上需要的硬币
- for (int j=0;j<n;j++)
- {
- if (i>=coin[j])//找零的钱大于这个硬币的面值
- {
- if( f[i-coin[j]]+1<=minNum) {
- //在更新时,需要判断i-coin[j]是否能找的开,如果找不开,就不需要更新
- if (i-coin[j] == 0 || coinValue[i-coin[j]]!=0 )
- {
- minNum = f[i-coin[j]] + 1;//更新
- curMoney = coin[j];//更新
- }
- }
- }
- }
- f[i]=minNum;
- coinValue[i]=curMoney;
- }
- //输出结果
- if(coinValue[money]==0)
- cout<<“找不开零钱”<<endl;
- else
- {
- cout<<“需要最少硬币个数为:”<<f[money]<<endl;
- cout<<“硬币分别为:”;
- while(money>0)
- {
- cout<<coinValue[money]<<“,”;
- money-=coinValue[money];
- }
- }
- delete []f;
- delete []coinValue;
- }
- int main()
- {
- int Money=18;
- int coin[]={1,2,5,9,10};
- FindMin(Money,coin,5);
- }
解法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];
}