动态规划:最少硬币找零问题、01背包问题、完全背包问题

题目一: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
  1. #include<iostream>  
  2. using namespace std;  
  3. //money需要找零的钱  
  4. //coin可用的硬币  
  5. //硬币种类  
  6. void FindMin(int money,int *coin, int n)  
  7. {  
  8.     int *f =new int[money+1]();//存储1...money找零最少需要的硬币的个数  
  9.     int *coinValue=new int[money+1]();//最后加入的硬币,方便后面输出是哪几个硬币  
  10.     coinNum[0]=0;  
  11.   
  12.     for(int i=1; i<=money; i++)  
  13.     {  
  14.         int minNum=i;//表示i个1元硬币正好找零i元,这是一种找零的组合,并不是所有组合里面最少的,下面寻找最少值
  15.         int curMoney=0;//这次找零,在原来的基础上需要的硬币  
  16.         for (int j=0;j<n;j++)  
  17.         {  
  18.             if (i>=coin[j])//找零的钱大于这个硬币的面值  
  19.             {  
  20.                 if( f[i-coin[j]]+1<=minNum) {
  21.                     //在更新时,需要判断i-coin[j]是否能找的开,如果找不开,就不需要更新
  22.                     if (i-coin[j] == 0 || coinValue[i-coin[j]]!=0 )  
  23.                     {  
  24.                         minNum = f[i-coin[j]] + 1;//更新  
  25.                        curMoney = coin[j];//更新  
  26.                     }  
  27.                 }
  28.             }  
  29.         }  
  30.         f[i]=minNum;  
  31.         coinValue[i]=curMoney;  
  32.     }  
  33.   
  34.     //输出结果  
  35.     if(coinValue[money]==0)  
  36.         cout<<"找不开零钱"<<endl;  
  37.     else  
  38.     {  
  39.         cout<<"需要最少硬币个数为:"<<f[money]<<endl;  
  40.         cout<<"硬币分别为:";  
  41.         while(money>0)  
  42.         {  
  43.             cout<<coinValue[money]<<",";  
  44.             money-=coinValue[money];  
  45.         }  
  46.     }  
  47.     delete []f;  
  48.     delete []coinValue;  
  49. }  
  50. int main()  
  51. {  
  52.     int Money=18;  
  53.     int coin[]={1,2,5,9,10};  
  54.     FindMin(Money,coin,5);  
  55.   
  56. }  

解法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];
    }


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值