有关货币问题的动态规划题目--有关01背包,完全背包,多重背包

背包dp:参考背包九讲以及给出一些题目

01背包 (先枚举物品,再逆序枚举容量)

给定n件物品和一个容量为V的背包,每件物品的体积是w[i],价值是va[i](1<=i<=n),求在不超过背包的容量的情况下,怎么选择装这些物品使得得到的价值最大?

例如:有5件物品,体积分别是{2,2,6,5,4},价值分别是{6,3,5,4,6}

递归式:F(i,v)=max(F(i-1,v), F(i-1,v-w[i])+va[i]),其中F(i,v)表示把前i种物品恰放入背包容量为v时取得的最大价值

把这个过程理解下:在前i件物品放进容量v的背包时,

它有两种情况:

第一种是第i件不放进去,这时所得价值为:f[i-1][v]

第二种是第i件放进去,这时所得价值为:f[i-1][v-w[i]]+va[i]

(第二种是什么意思?就是如果第i件放进去,那么在容量v-w[i]里就要放进前i-1件物品)

最后比较第一种与第二种所得价值的大小,哪种相对大,f[i][v]的值就是哪种。

能用二维数组去解,也能优化为一维的数组

01背包是基础,需要好好的理解。

完全背包(先枚举物品,再正序枚举容量):

给定n种物品和一个容量为V的背包,每种物品有无限个,体积是w[i],价值是va[i](1<=i<=n),求在不超过背包的容量的情况下,怎么装物品进背包使得获得的价值最大?

递归式①:F(i,v)=max{F(i-1, v-k*w[i])+k*va[i] | 0<=k*w[i]<=v}

递归式②:F(i,v)=max(F(i-1, v), F(i, v-w[i])+va[i])

 

多重背包(先枚举物品,再正序枚举容量):

给定n种物品和一个容量为V的背包,每种物品有t[i]个,体积是w[i],价值是va[i](1<=i<=n),求在不超过背包的容量的情况下,怎么装物品进背包使得获得的价值最大?

递归式:F(i,v)=max{F(i-1, v-k*w[i])+k*va[i] | 0<=k<=t[i]}

 

以下是汇集了四道货币类型的背包,通过一些解题思路来加深理解以上三种背包,比较适合有了解了上述的背包裸题的同学看。

1(01背包类)给你一张n元的货币,让你去杂货店买东西,杂货店有t件东西,每件东西有自己的价格,(购买的时候须整件的买,不能拆分),可是杂货店老板没有零钱,所以你需要用你手中的n元货币买到最多价值的东西,问你能买到的最大价值是多少?

 

思路:这道题是典型的01背包的简化版,因为可以抽象为,将t件拥有不同体积的物品有选择的放入容量n的背包,使得背包剩余的空间最小。

还是按n件物品的划分n个状态,对于当前第i件物品,容量为j的情况下,还是有放和不放两种策略,如果放了能使已装的容量达到最大,那就放,如果选择放进这件物品没能比不放它的时候的已装容量要大就不放。同样用opt[i][j]表示将前i件物品放入容量为j的背包所能获得的不超过背包的最大容量,v数组表示每件物品的体积

 

这里我们可以列出转移方程式:

opt[i][j]=/ opt[i-1][j]     if(j<v[i])

             \ max(opt[i-1][j], opt[i-1][j-v[i]]+v[i])   if(j>=v[i])

以下给出2维和1维的参考代码。

#include<iostream>
#include<cstring>
int opt[1000][1000],v[1000];
using namespace std;
int main()
{
    int n,t;
    while(cin>>n>>t) {
       int i,j;
       for(i=1;i<=t;i++) 
           cin>>v[i];
       memset(opt,0,sizeof(opt));
       for(i=1;i<=t;i++) {
           for(j=1;j<=n;j++) {
               if(j<v[i])
                  opt[i][j]=opt[i-1][j];
               else
                  opt[i][j]=max(opt[i-1][j], opt[i-1][j-v[i]]+v[i]);
           }
       }
       cout<<opt[t][n]<<endl;
    }
    return 0;
}

代码1维:

#include<iostream>
#include<cstring>
int opt[1000],v[1000];
using namespace std;
int main()
{
    int n,t;
    while(cin>>n>>t) {
       int i,j;
       for(i=1;i<=t;i++) 
           cin>>v[i];
       memset(opt,0,sizeof(opt));
       for(i=1;i<=t;i++) {
           for(j=n;j>=v[i];j--) { 
                opt[j]=opt[j] > opt[j-v[i]]+v[i] ? opt[j] :opt[j-v[i]]+v[i];
                if(i==t&&j==n)
                break; 
           }
       }
       cout<<opt[n]<<endl;
    }
    return 0;
}

2)(完全背包类)给定4种面值的货币(1元,2元,3元,5元)无限张,要求凑N元,有多少种凑法?

这个是完全背包的模型。用opt[i][j]表示用前i种钱币来凑得j元的方法数

思路:举个例子,N=6,(建议自己试着填完之后再参考下面的)

j

 i

0

1

2

3

4

5

6

v[1]=1

1

1

1

1

1

1

1

v[2]=2

1

1

2

2

3

3

4

v[3]=3

1

1

2

3

4

5

7

v[4]=5

1

1

2

3

4

6

8

注:

表格上对应的要填的值是opt[i][j],可知opt[i][0]=1(1<=i<=4),这个表示,用前i种货币去凑0元的方法数只有一种:不用货币,这也是一种方法。

对应的自己填下值,留意下填表的顺序。

分析方法:在前 i 种货币凑 j 元的状态中,如果j小于第i种货币面值,opt[i][j]就不能用这第i种货币凑钱,方法数还是不变;如果j大于第i种货币面值,那么opt[i][j]就是仅用前i-1种货币凑钱得到的方法数和用了第i种货币凑钱得到的方法数之和。到了这里可以我们可以写出

二维的状态转移方程:

opt[i][j]=/ opt[i-1][j]  if(j<v[i])

             \ opt[i-1][j]+opt[i][j-v[i]]   if(j>=v[i])

自己写出的程序还应当适用于给出无序的几种货币面值的情况。

主要的二维代码段如下:

v[14]={1,2,3,5};
opt[04][0..N]=0;
opt[14][0]=1;
for(i=1;i<=4;i++) {
    for(j=1;j<=N;j++) {
       if(j<v[i])
           opt[i][j]=opt[i-1][j];
       else
           opt[i][j]=opt[i-1][j]+opt[i][j-v[i]];}
}
opt[4][N]为所求

我们还可以从二维优化到一维,以下是代码的主要部分:

/*自己注意下数组初始化*/
for(i=1;i<=4;i++) {
     for(j=v[i];j<=N;j++) { /*2*/
          opt[j]=opt[j]+opt[j-v[i]];
     }
}

3)(多重背包类)给定1元,2元,3元,5元,10元,20元面值的货币若干张,求用他们能称出的钱的种类数

例如给出:1 1 0 0 0 0 ,能凑出的钱是3种(1元,2元,3元)

注:

这个是多重背包问题(每种货币有若干张),问题问的是能称出的钱的种类数,由于每种货币有固定的张数,可以将多重背包转化为01背包来求解。

例如给出货币对应的个数0,2,1,3,1,0,自己试着推结果应为25,过程如下:

一共有2,2,3,5,5,5,10的7张货币让我们凑,能凑到的最大的钱数是sum=2+2+3+5+5+5+10=32元,那么这道题可以理解为在1~ sum这个范围内,计算用以上的7张货币能凑整钱的有多少个数。其中可以用opt[num][j]来表示有前num个货币(由前i种构成)来凑到j元的状态,凑得到为1,凑不到就为0,当num=1时,能凑到1种(2元,其中opt[1][1]=0, opt[1][2]=opt[0][2-2]=1);

当num=2时,能凑到2种(2元,4元,其中,

opt[2][1]=opt[1][1]=0,

opt[2][2]=opt[1][2] 或者opt[1][2-2]=1,

opt[2][3]=opt[1][3] 或者 opt[1][3-2]=0,

opt[2][4]=opt[1][4] 或者 opt[1][4-2]=1)

当num=3时,能凑到4种(2元,4元,5元,7元,其中,

opt[3][1]=opt[2][1]=0,

opt[3][2]=opt[2][2]=1,

opt[3][3]=opt[2][3] 或者 opt[2][3-3]=1,

opt[3][4]=opt[2][4] 或者 opt[2][4-3]=0,

opt[3][5]=opt[2][5] 或者 opt[2][5-3]=1,

opt[3][6]=opt[2][6] 或者 opt[2][6-3]=0,

opt[3][7]=opt[2][7] 或者 opt[2][7-3]=1)

当num=4时(后面的情况省略)

(以上过程自己好好理解)

状态转移方程: opt[num][j]= / opt[num-1][j]     if  j<v[i]

                                        \  1   if  opt[num-1][j] || opt[num-1][j-v[i]]

其中我们可以将其转换为01背包来解,以下给出2维的代码:

v[16]={1,2,3,5,10,20};
n[16];   /*6种货币对应的个数*/
opt[…][0]=1;         /*用多少货币凑0元都有一个合法状态,就是什么货币都不加*/
for(i=1;i<=6;i++) {
    for(j=1;j<=n[i];j++) {
          num+=1; 
          sum+=v[i];
      /*以下类似01背包*/
        for(k=1;k<=sum;k++) {
            if(k<v[i])
                opt[num][k]=opt[num-1][k];
            else if(opt[num-1][k] || opt[num-1][k-v[i]])
                opt[num][k]=1;
         }
     }
}
ans+=opt[count][1…sum]==1?1:0;

以下是一维部分的代码:

opt[0]=1;       /*合法状态*/
for(i=1;i<=6;i++) {
      for(j=1;j<=n[i];j++) {
           num+=1; 
           sum+=v[i];
         /*以下类似01背包*/
           for(k=sum;k>=v[i];k--) {   /*3*/
               if(opt[k] || opt[k-v[i]])   
                    opt[k]=1;
            }
       }
}

4有面值为1元、3元和5元的硬币无数个,如何用最少的硬币凑够11元?(其中保证给出的钱一定能凑整11元的)

分析:遇到题可以还是从填表做起,以下两个表建议先自己填完在参考给出的,填表的过程可以让你自己了解一些思路。(注意写出的程序应该也要能够处理给定面值的货币不是升序的情况~)

 

1

2

3

4

5

6

7

8

9

10

11

1

1

2

3

4

5

6

7

8

9

10

11

3

1

2

1

2

3

2

3

4

3

4

5

5

1

2

1

2

1

2

3

2

3

2

3

 

 

1

2

3

4

5

6

7

8

9

10

11

5

0

0

0

0

1

0

0

0

0

2

0

3

0

0

1

0

1

2

0

2

3

2

3

1

1

2

1

2

1

2

3

2

3

2

3

 

这里我们给出了两个表,填完之后是不是有些感觉了呢?

这道题给出的是一定面值的货币种类,张数不限。还是可以按货币种类划分阶段(这里是3种货币,就抽象成三个阶段),这里拿表2来讲解,因为其具有一般性。其中用opt[i]表示凑i元最少需要的张数。

在阶段2的时候,可用面值是3元和5元,opt[8]由0更新为2(opt[8]=opt[8-3]+1=2),而opt[7]的时候,还是为0,因为假设当前阶段(加入3元面值)中,7元试图兑换成一张3元和最少张数换得的4元,但是4元暂时还没办法兑换到(opt[4]=0),所以导致opt[7]仍为0。

也就是在某一阶段中,先看看新加入的面值v[i]参与凑n元的时候能否成功(即张数不为0),成功的话得到的张数t1再和没有v[i]货币参与兑换获得的最少张数t2相比较(也就是当前状态的前一个状态),若t2本为0,则能凑成n元需要的最少张数更新为t1;若t2不为0,就取min(t1, t2)来更新凑成n元需要的最少张数

看完上述的3道例题,这里可以自己试着推导写出程序。

#include<iostream>
#include<cstring>
using namespace std;
int v[1000];
int opt[1000];
int min(int a,int b)
{
    return a<b?a:b;
}
int main()
{
    int n, t;
    while(cin>>n>>t) {
           int i,j,temp;
           memset(opt,0,sizeof(opt));
        for(i=1;i<=t;i++)
           cin>>v[i];
        for(i=1;i<=t;i++) {
           for(j=v[i];j<=n;j++) {
                if(j==v[i])
                   opt[j]=1;
                else {
                   if(opt[j-v[i]]) {
                      temp=opt[j-v[i]]+1;
                      opt[j]=opt[j]?min(opt[j],temp):temp;
                   }
                }
           }
        }
        cout<<opt[n]<<endl;
    }
    return 0;
}
View Code

补充思考:二维到一维的改变

看完上面的4道例题之后,二维转换为一维的时候,我们发现这个对算法的空间复杂度和时间复杂度都有较大的提升,其中空间的复杂度中一维的实现我们叫做滚动数组。这里要讲的是,01背包opt[j]的值j采用逆序循环,完全背包opt[j]中j是顺序循环,为什么要这样呢?

下面以例1和例2进行分析,

例题1:

for(i=1;i<=t;i++) {

   for(j=n;j>=v[i];j--) {                    /*1*/

       opt[j]=max(opt[j], opt[j-v[i]]+v[i]);

       /* if(i==t && j==n)  break;  */

   }

}

 

例题2:

for(i=1;i<=4;i++) {

     for(j=v[i];j<=N;j++) {              /*2*/

      opt[j]=opt[j]+opt[j-v[i]];

      }

}

 

实际上可以从它们的状态转移方程来理解:

例题1的:opt[i][j]=/ opt[i-1][j]     if(j<v[i])

                           \max(opt[i-1][j],opt[i-1][j-v[i]]+v[i])   if(j>=v[i])

这里是计算第i个状态中的值,如果将opt数组转换为二维矩阵,那就是(i, j)点处的值是由(i-1,j)的值和(i-1,j-v[i])+v[i])的值决定的,也就是第i个状态仅需要用到i-1个状态的值,所以这里可以用逆序的从n到v[i]的过程来计算。

 

例题2的:opt[i][j]=/ opt[i-1][j]  if(j<v[i])

                          \ opt[ i-1 ][j]+opt[ i][j-v[i]]   if(j>=v[i])

这里也是计算第i个状态的值,可以将其转换为二维矩阵,这时我们发现,(i, j)处的值是由(i-1, j)处的值和(i, j-v[i])处的值共同决定的,也就是求解第i个状态需要用到i-1状态的值,也要用到同状态中i个状态中前面已经算过的值,所以这里需顺序的计算

 

总结:一些需要注意的技巧和问题:

1. 测试数据中有可能同一组数据测两次的

2. 遇到问题什么头绪都没有的时候,不妨自己枚举一下它的一些例子,看看能不能找出规律,也就是可以填表模拟一下过程,从中找出局部关系

3. 初期学习背包期望能把二维的和一维的写法都练习,掌握一维的写法,时空效率都有较大的提升

题目链接:

01背包题目:

Hdu 2602 Bone Collector 非常常规的01背包问题,用一维和二维数组都可以做,一维快相当多。解题报告

Hdu 2546 饭卡 n种菜选若干种使剩下的钱最少,背包容量是开始时的钱,物品体积是菜的价格,状态转移时记录答案 解题报告

Hdu 2955 Robberies (推荐)抢劫方案最优问题,需要一个简单地转换,我们求的是不被抓的概率而非被抓的概率,各个银行的储蓄总和为背包容量,体积为单个银行 的储蓄,价值为不被抓概率。解题报告

hdu 3466 与顺序有关的01背包,先按q-p排序再来处理,难想容易敲。解题报告

 

完全背包题目:

Uva 674 Coin Change 完全背包求解方案数问题,只有5种硬币,基础题。   解题报告

Uva 147 Dollars 硬币有11种,另外这题用double输入,要考虑精度问题。   解题报告

 

多重背包题目:

Hdu 1059 Dividing 简单多重背包,体积为硬币数,价值为币值,可用二进制处理成01背包求解,可用30对num进行优化。   解题报告

Poj 1276 Cash Machine 多重背包,需用二进制处理成01背包求解,体积是硬币数量,价值是币值。

Hdu 1114 Piggy-Bank  简单多重背包,但当成01背包来暴力也完全没有问题,

Hdu 2191 悼念512汶川大地震遇难同胞——珍惜现在,感恩生活 标题超长超简单的多重背包,可用01背包求解。

题目是参考 http://blog.csdn.net/liuqiyao_01/article/details/8477725 给出的,以上的四道题目自己理了下,有参考网上的一些资料,有发现错误的话请帮忙指出,谢谢

转载于:https://www.cnblogs.com/hstcacm/p/4790331.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值