有关货币问题的动态规划题目--有关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,(建议自己试着填完之后再参考下面的)
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[1…4]={1,2,3,5}; opt[0…4][0..N]=0; opt[1…4][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[1…6]={1,2,3,5,10,20}; n[1…6]; /*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道例题,这里可以自己试着推导写出程序。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#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; }
补充思考:二维到一维的改变
看完上面的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 给出的,以上的四道题目自己理了下,有参考网上的一些资料,有发现错误的话请帮忙指出,谢谢