算法与数据结构---3、砝码称重
一、总结
一句话总结:
砝码称重有基本的枚举解法,也有对应的01背包和多重背包的解法,对背包我们可以进行空间优化,对这题也可以进行bitset优化
/* C++的 bitset 在 bitset 头文件中,它是一种类似数组的结构, 它的每一个元素只能是0或1,每个元素仅用1bit空间 001000010010001000100100010001 如果选择状态: f[i][j]表示前i件物品中总重量为j方案的方案是否存在 那么f[i][j]里面存的数据也就是0和1,所以可以用bitset来代替 二维的状态转移方程空间优化之后 f[i][j]=f[i-1][j] || f[i-1][j-w[i]]; 就变成一维的 f[j]=[j] || [j-w[i]]; 解决j-w[i]>=0之后 f[j+a[i]]=f[j+a[i]] || f[j]; 空间优化之后,也可以写成 if(f[j]) f[j+a[i]]=1; 这个时候,f[]这个数组就可以用bitset来代替了 0000000000000000000000000000001 注意: for(int i=1;i<=6;i++){ //对不同型号的砝码进行循环 for(int k=1;k<=n[i];k++){ //对同一个型号的多个砝码进行循环 for(int j=weight;j>=a[i];j--){ f[j]=f[j] || f[j-a[i]]; } } } 使用bitset优化砝码称重的多重背包解法的时候,注意点是什么 要解决bitset去掉的那层循环的限制条件,也就是第三层循环中的j>=a[i], 可以把f[j]=f[j]||f[j-a[i]]中j的位置都加上a[i]即可解决 在砝码称重的bitset的优化中,bitset的左移和右移操作如何确定 由砝码称重这题对应的关系,if(f[j]) f[j+a[i]]=1,由f[j]得到f[j+a[i]],那么肯定是左移 */ #include <iostream> #include <bitset> using namespace std; bitset<1005> f; int main(){ int n[7]; int a[7]={0,1,2,3,5,10,20}; for(int i=1;i<=6;i++){ cin>>n[i]; } //2、初始化动态规划数组,做动态规划 f[0]=1; for(int i=1;i<=6;i++){ //对不同型号的砝码进行循环 for(int k=1;k<=n[i];k++){ //对同一个型号的多个砝码进行循环 f=f | f<<a[i]; } } //3、统计方案总数 cout<<"Total="<<(f.count()-1)<<endl; return 0; }
1、使用bitset优化砝码称重的多重背包解法的时候,注意点是什么?
a、要解决bitset去掉的那层循环的限制条件,也就是第三层循环中的j>=a[i],
b、可以把f[j]=f[j]||f[j-a[i]]中j的位置都加上a[i]即可解决
|||-begin
for(int i=1;i<=6;i++){ //对不同型号的砝码进行循环 for(int k=1;k<=n[i];k++){ //对同一个型号的多个砝码进行循环 for(int j=weight;j>=a[i];j--){ f[j]=f[j] || f[j-a[i]]; } } }
|||-end
2、在砝码称重的bitset的优化中,bitset的左移和右移操作如何确定?
由砝码称重这题对应的关系,if(f[j]) f[j+a[i]]=1,由f[j]得到f[j+a[i]],那么肯定是左移
二、砝码称重
博客对应课程的视频位置:
3.1、砝码称重-枚举法
https://www.fanrenyi.com/video/27/254
3.2、砝码称重-01背包
https://www.fanrenyi.com/video/27/255
3.3、砝码称重-01背包2
https://www.fanrenyi.com/video/27/256
3.4、砝码称重-01背包空间优化
https://www.fanrenyi.com/video/27/257
3.5、砝码称重-多重背包
https://www.fanrenyi.com/video/27/258
3.6、砝码称重-进一步优化
https://www.fanrenyi.com/video/27/259
3.7、砝码称重-bitset优化
https://www.fanrenyi.com/video/27/262
1、题目需求
砝码称重(NOIP1996)
【问题描述】
设有1g、2g、3g、5g、10g、20g的砝码各若干枚(其总重<=1000),
求用这些砝码能称出不同的重量的个数。
【输入文件】
输入1g、2g、3g、5g、10g、20g的砝码个数
【输出文件】
能称出不同的重量的个数
【输入样例】
1 1 0 0 0 0
【输出样例】
3
题目提交位置
https://www.luogu.com.cn/problem/P2347
2、枚举法解法
1 /* 2 3 分析 4 根据输入的砝码信息,每种砝码可用的最大个数是确定的,而且每种砝码的个数是连续的, 5 能取0到最大个数,所以,符合穷举法的两个条件,可以使用穷举法。 6 7 穷举时,重量可以由1g,2g,……,20g的砝码中的任何一个,或者多个构成, 8 枚举对象可以确定为6种重量的砝码,范围为每种砝码的个数,判定时, 9 只需判断这次得到的重量是新得到的,还是前一次已经得到的,即判重。 10 由于总重<=1000,所以,可以开一个flag[1..1000]的布尔数组来判重, 11 当得到v重量时,把flag[v]置为true,下次再得到v时,还是置true, 12 最后只需遍历一下flag数组,即可得到重量的个数。 13 14 枚举变量:1g砝码,2g砝码,3g砝码,5g砝码,10g砝码,20g砝码 15 枚举范围:1g砝码0-n1,2g砝码0-n2,3g砝码0-n3,5g砝码0-n5,10g砝码0-n10,20g砝码0-n20 16 枚举判断条件: 17 统计所有砝码和的不同重量 18 砝码和总重小于1000 19 20 算法思路: 21 1、枚举不同砝码的个数,计算总重量,并将总重量对应的标志置为1 22 2、根据标志,计算总重量的个数 23 24 25 100 100 150 250 100 300 26 100*50*50*50*10*15 27 28 */ 29 #include <iostream> 30 using namespace std; 31 int flag[1005] = {0}; 32 int main() 33 { 34 int n1, n2, n3, n5, n10, n20; 35 cin >> n1 >> n2 >> n3 >> n5 >> n10 >> n20; 36 //1、枚举不同砝码的个数,计算总重量,并将总重量对应的标志置为1 37 for (int i1 = 0; i1 <= n1; i1++) 38 { 39 for (int i2 = 0; i2 <= n2; i2++) 40 { 41 for (int i3 = 0; i3 <= n3; i3++) 42 { 43 for (int i5 = 0; i5 <= n5; i5++) 44 { 45 for (int i10 = 0; i10 <= n10; i10++) 46 { 47 for (int i20 = 0; i20 <= n20; i20++) 48 { 49 int sum=i1+i2*2+i3*3+i5*5+i10*10+i20*20; 50 flag[sum]=1; 51 } 52 } 53 } 54 } 55 } 56 } 57 //2、根据标志,计算总重量的个数 58 int count=0; 59 for(int i=1;i<=1000;i++){ 60 if(flag[i]) count++; 61 } 62 cout<<"Total="<<count<<endl; 63 return 0; 64 }
3、01背包解法
1 /* 2 砝码称重(NOIP1996) 3 4 【问题描述】 5 设有1g、2g、3g、5g、10g、20g的砝码各若干枚(其总重<=1000), 6 求用这些砝码能称出不同的重量的个数。 7 【输入文件】 8 输入1g、2g、3g、5g、10g、20g的砝码个数 9 【输出文件】 10 能称出不同的重量的个数 11 【输入样例】 12 1 1 0 0 0 0 13 【输出样例】 14 3 15 16 17 分析: 18 19 比如2 1 2 1 1 1的输入数据,就是表示1 1 2 3 3 5 10 20 20 21 问题就转化为 1 1 2 3 3 5 10 20 这些砝码, 22 对里面的每一个取或者不取,可以组成多少个总重量 23 那么这就是一个非常标准的01背包问题 24 25 f[i][j]表示前i件物品中总重量为j方案的方案是否存在 26 27 如 f[4][5]就是表示前4件物品中总重量为5的方案是否存在 28 29 状态转移方程: 30 当第i件物品不取的时候: 31 如果f[i-1][j]存在 f[i][j]=1 32 当第i件物品取的时候: 33 如果f[i-1][j-w[i]]存在,f[i][j]=1 34 35 所以f[i][j]=f[i-1][j] || f[i-1][j-w[i]]; 36 初始状态: 37 f[k][0]=1 38 终止状态: 39 设砝码的总个数为num个 40 f[num][1000] 41 当然这个还不是直接所求的, 42 直接所求就是求1到1000的过程中,哪一些砝码总数的方案不为0即可 43 44 45 算法思路: 46 1、统计砝码总数,准备好砝码序列 47 2、初始化动态规划数组,做动态规划 48 3、统计方案总数 49 50 51 */ 52 #include <iostream> 53 using namespace std; 54 int f[1005][1005]={0}; 55 int main(){ 56 //1、统计砝码总数,准备好砝码序列 57 int num=0;//砝码总数 58 int w[1005];//砝码序列 59 int a[7]={0,1,2,3,5,10,20}; 60 for(int i=1;i<=6;i++){ 61 int x; 62 cin>>x; 63 for(int j=1;j<=x;j++) w[++num]=a[i]; 64 } 65 //2、初始化动态规划数组,做动态规划 66 for(int i=0;i<=1000;i++) f[i][0]=1; 67 for(int i=1;i<=num;i++){ 68 for(int j=1;j<=1000;j++){ 69 if(j-w[i]>=0) 70 f[i][j]=f[i-1][j] || f[i-1][j-w[i]]; 71 else 72 f[i][j]=f[i-1][j]; 73 } 74 } 75 //3、统计方案总数 76 int count=0; 77 for(int i=1;i<=1000;i++){ 78 if(f[num][i]) count++; 79 } 80 cout<<"Total="<<count<<endl; 81 return 0; 82 }
4、01背包另一种思路
1 /* 2 3 分析: 4 5 f[i][j]表示前i件物品中总重量为j方案的方案是否存在 6 7 到 8 9 f[i][j]表示前i件物品中总重量为j方案总数 10 11 12 ================================================== 13 ================================================== 14 15 16 比如2 1 2 1 1 1的输入数据,就是表示1 1 2 3 3 5 10 20 17 18 问题就转化为 1 1 2 3 3 5 10 20 这些砝码, 19 对里面的每一个取或者不取,可以组成多少个总重量 20 那么这就是一个非常标准的01背包问题 21 22 f[i][j]表示前i件物品中总重量为j方案总数 23 如 f[4][5]就是表示前4件物品中总重量为5的方案总数 24 (这里我们设置状态设置的是总重量为j方案总数, 25 方案总数只要大于等于1,那么就说明重量为j的方案是存在的) 26 27 状态转移方程: 28 当第i件物品不取的时候: 29 f[i][j]=f[i-1][j] 30 当第i件物品取的时候: 31 f[i][j]=f[i-1][j-w[i]] 32 33 所以f[i][j]=f[i-1][j] + f[i-1][j-w[i]]; 34 初始状态: 35 f[k][0]=1 36 终止状态: 37 设砝码的总个数为num个 38 f[num][1000] 39 当然这个还不是直接所求的, 40 直接所求就是求1到1000的过程中,哪一些砝码总数的方案不为0即可 41 42 43 算法思路: 44 1、统计砝码总数,准备好砝码序列 45 2、初始化动态规划数组,做动态规划 46 3、统计方案总数 47 48 */ 49 50 51 #include <iostream> 52 using namespace std; 53 int f[1005][1005]={0}; 54 int main(){ 55 //1、统计砝码总数,准备好砝码序列 56 int num=0;//砝码总数 57 int w[1005];//砝码序列 58 int a[7]={0,1,2,3,5,10,20}; 59 for(int i=1;i<=6;i++){ 60 int x; 61 cin>>x; 62 for(int j=1;j<=x;j++) w[++num]=a[i]; 63 } 64 //2、初始化动态规划数组,做动态规划 65 for(int i=0;i<=1000;i++) f[i][0]=1; 66 for(int i=1;i<=num;i++){ 67 for(int j=1;j<=1000;j++){ 68 if(j-w[i]>=0) 69 f[i][j]=f[i-1][j] + f[i-1][j-w[i]]; 70 else 71 f[i][j]=f[i-1][j]; 72 } 73 } 74 //3、统计方案总数 75 int count=0; 76 for(int i=1;i<=1000;i++){ 77 if(f[num][i]) count++; 78 } 79 cout<<"Total="<<count<<endl; 80 return 0; 81 }
5、01背包的空间优化
1 /* 2 3 分析: 4 5 01背包本身是可以进行空间优化的 6 因为动态规划本质上是填表 7 f[i][j]表示前i件物品中总重量为j方案总数 8 f[i][j]=f[i-1][j] + f[i-1][j-w[i]]; 9 填表的顺序为: 10 i是从1-num 11 j是从1-1000 12 是用的2维的表格 13 01背包问题用一维表格也可以实现保存中间状态 14 具体实现就是去掉i这一维, 15 16 f[j]=f[j] + [j-w[i]]; 17 只不过这个时候,填表的顺序就是 18 i是从1-num 19 j是从1000-1 20 21 */ 22 #include <iostream> 23 using namespace std; 24 int f[1005]={0}; 25 int main(){ 26 //1、统计砝码总数,准备好砝码序列 27 int num=0;//砝码总数 28 int w[1005];//砝码序列 29 int a[7]={0,1,2,3,5,10,20}; 30 for(int i=1;i<=6;i++){ 31 int x; 32 cin>>x; 33 for(int j=1;j<=x;j++) w[++num]=a[i]; 34 } 35 //2、初始化动态规划数组,做动态规划 36 f[0]=1; 37 for(int i=1;i<=num;i++){ 38 for(int j=1000;j>=1;j--){ 39 if(j-w[i]>=0) 40 f[j]=f[j] + f[j-w[i]]; 41 } 42 } 43 //3、统计方案总数 44 int count=0; 45 for(int i=1;i<=1000;i++){ 46 if(f[i]) count++; 47 } 48 cout<<"Total="<<count<<endl; 49 return 0; 50 }
6、多重背包解法
1 /* 2 砝码称重(NOIP1996) 3 4 【问题描述】 5 设有1g、2g、3g、5g、10g、20g的砝码各若干枚(其总重<=1000), 6 求用这些砝码能称出不同的重量的个数。 7 【输入文件】 8 输入1g、2g、3g、5g、10g、20g的砝码个数 9 【输出文件】 10 能称出不同的重量的个数 11 【输入样例】 12 1 1 0 0 0 0 13 【输出样例】 14 3 15 16 17 分析 18 这个问题本身的描述就是一个多重背包, 19 也就是每个砝码有多个 20 21 多重背包就不需要01背包里面的 1、统计砝码总数,准备好砝码序列 22 23 多重背包可以在01背包的基础上稍微改一下就实现了 24 25 */ 26 27 #include <iostream> 28 using namespace std; 29 int f[1005]={0}; 30 int main(){ 31 32 int n[7]; 33 int a[7]={0,1,2,3,5,10,20}; 34 for(int i=1;i<=6;i++){ 35 cin>>n[i]; 36 } 37 //2、初始化动态规划数组,做动态规划 38 f[0]=1; 39 for(int i=1;i<=6;i++){ //对不同型号的砝码进行循环 40 for(int k=1;k<=n[i];k++){ //对同一个型号的多个砝码进行循环 41 for(int j=1000;j>=a[i];j--){ 42 f[j]=f[j] + f[j-a[i]]; 43 } 44 } 45 } 46 //3、统计方案总数 47 int count=0; 48 for(int i=1;i<=1000;i++){ 49 if(f[i]) count++; 50 } 51 cout<<"Total="<<count<<endl; 52 return 0; 53 }
7、进一步优化
1 /* 2 3 题中说总重<=1000,所以我们的动态规划根据这个1000做循环, 4 实际上,我们可以根据给的输入数据里面的砝码重量做循环, 5 因为砝码重量总是小于等于1000的,所以可以进行一定程度的优化 6 7 8 */ 9 10 #include <iostream> 11 using namespace std; 12 int f[1005]={0}; 13 int main(){ 14 15 int n[7]; 16 int weight=0; 17 int a[7]={0,1,2,3,5,10,20}; 18 for(int i=1;i<=6;i++){ 19 cin>>n[i]; 20 weight+=n[i]*a[i]; 21 } 22 //2、初始化动态规划数组,做动态规划 23 f[0]=1; 24 for(int i=1;i<=6;i++){ //对不同型号的砝码进行循环 25 for(int k=1;k<=n[i];k++){ //对同一个型号的多个砝码进行循环 26 for(int j=weight;j>=a[i];j--){ 27 f[j]=f[j] + f[j-a[i]]; 28 } 29 } 30 } 31 //3、统计方案总数 32 int count=0; 33 for(int i=1;i<=weight;i++){ 34 if(f[i]) count++; 35 } 36 cout<<"Total="<<count<<endl; 37 return 0; 38 }
8、bitset优化
1 /* 2 3 C++的 bitset 在 bitset 头文件中,它是一种类似数组的结构, 4 它的每一个元素只能是0或1,每个元素仅用1bit空间 5 6 001000010010001000100100010001 7 8 9 如果选择状态: 10 f[i][j]表示前i件物品中总重量为j方案的方案是否存在 11 12 那么f[i][j]里面存的数据也就是0和1,所以可以用bitset来代替 13 14 二维的状态转移方程空间优化之后 15 f[i][j]=f[i-1][j] || f[i-1][j-w[i]]; 16 就变成一维的 17 f[j]=[j] || [j-w[i]]; 18 19 解决j-w[i]>=0之后 20 f[j+a[i]]=f[j+a[i]] || f[j]; 21 空间优化之后,也可以写成 22 if(f[j]) f[j+a[i]]=1; 23 24 25 这个时候,f[]这个数组就可以用bitset来代替了 26 27 28 0000000000000000000000000000001 29 30 注意: 31 for(int i=1;i<=6;i++){ //对不同型号的砝码进行循环 32 for(int k=1;k<=n[i];k++){ //对同一个型号的多个砝码进行循环 33 for(int j=weight;j>=a[i];j--){ 34 f[j]=f[j] || f[j-a[i]]; 35 } 36 } 37 } 38 使用bitset优化砝码称重的多重背包解法的时候,注意点是什么 39 要解决bitset去掉的那层循环的限制条件,也就是第三层循环中的j>=a[i], 40 可以把f[j]=f[j]||f[j-a[i]]中j的位置都加上a[i]即可解决 41 42 在砝码称重的bitset的优化中,bitset的左移和右移操作如何确定 43 由砝码称重这题对应的关系,if(f[j]) f[j+a[i]]=1,由f[j]得到f[j+a[i]],那么肯定是左移 44 45 */ 46 47 48 #include <iostream> 49 #include <bitset> 50 using namespace std; 51 bitset<1005> f; 52 int main(){ 53 54 int n[7]; 55 int a[7]={0,1,2,3,5,10,20}; 56 for(int i=1;i<=6;i++){ 57 cin>>n[i]; 58 } 59 //2、初始化动态规划数组,做动态规划 60 f[0]=1; 61 for(int i=1;i<=6;i++){ //对不同型号的砝码进行循环 62 for(int k=1;k<=n[i];k++){ //对同一个型号的多个砝码进行循环 63 f=f | f<<a[i]; 64 } 65 } 66 //3、统计方案总数 67 68 cout<<"Total="<<(f.count()-1)<<endl; 69 return 0; 70 }