背包系列专题之完全背包问题

专题二:完全背包问题
/*
	Name: 背包系列专题之完全背包问题
	Author: 巧若拙
	Description: 完全背包问题:在n种物品中选取若干件(同一种物品可多次选取)放在容量为c的背包里,分别用P[i]和W[i]存储第i种物品的价值和重量。
求解怎么装物品可使背包里物品总价值最大。
样例输入
4 12
2 3
5 7
6 8
10 12
样例输出
18
*/
#include<iostream>

using namespace std;

const int MAXC = 10000; //背包最大容量 
const int MAXN = 2000; //物品的最大个数
int W[MAXN+1];//物品的重量 
int P[MAXN+1];//物品的价值 
int B1[MAXN+1][MAXC+1]; //记录给定n个物品装入容量为c的背包的最大价值 
int B2[MAXN+1][MAXC+1]; //记录给定n个物品装入容量为c的背包的最大价值 
int pre[MAXC+1]; //pre[j]相当于B2[i-1][j] 
int cur[MAXC+1]; //cur[j]相当于B2[i][j]
int B4[MAXC+1]; //记录最优解 

int CompletePack_1(int n, int c);//动态规划+朴素的穷举思想
int CompletePack_2(int n, int c);//动态规划:F[i][j] = max(F[i-1][j], F[i][j-W[i]] + P[i])
int CompletePack_3(int n, int c);//优化的动态规划算法,用2个一维数组代替二维数组 
int CompletePack_4(int n, int c);//优化的动态规划算法,一维数组存储记录

int main() 
{
	int n, c;
	cin >> n >> c;
	
	for (int i=1; i<=n; i++)//不计下标为0的元素 
	{
		cin >> W[i] >> P[i];
	}
	
	//动态规划+朴素的穷举思想,需要用到全局变量W[], P[], 另有B1[MAXN+1][]初始化为0
	cout << CompletePack_1(n, c) << endl; 
	
	//动态规划:二维数组存储记录,需要用到全局变量W[], P[], 另有B2[MAXN+1][]默认初始化为0 
	cout << CompletePack_2(n, c) << endl; 
	
	//动态规划:使用2个一维数组存储记录,需要用到全局变量W[], P[], 另有pre[]和cur[]均初始化为0
	cout << CompletePack_3(n, c) << endl; 
	
	//优化的动态规划算法,一维数组存储记录,需要用到全局变量W[], P[], 另有B4[]初始化为0
	cout << CompletePack_4(n, c) << endl; 
	
	return 0;
}

算法1:动态规划+朴素的穷举思想,需要用到全局变量W[], P[], 另有B1[MAXN+1][]初始化为0。
int CompletePack_1(int n, int c)//动态规划+朴素的穷举思想
{
 	for (int i=1; i<=n; i++)
	{
		for (int j=1; j<=c; j++) 
		{ 
			int bestP = 0;
			for (int k=0; k*W[i]<=j; k++) //语句1
			{
				if (bestP < B1[i-1][j-k*W[i]] + k*P[i])
					bestP = B1[i-1][j-k*W[i]] + k*P[i];
			}
			B1[i][j] = bestP;
		}
	}
	
	return B1[n][c];
}

问题1:语句1能否改为:for (int k=j/W[i]; k>=0; k--)?为什么?

参考答案:
问题1:可以,因为语句1所在循环体的作用是穷举装入第i种物品的数量,并计算出能获得的最大价值,循环变量k值递增或递减均可,实际上修改后的语句1效率更高。

算法2:动态规划:二维数组存储记录,需要用到全局变量W[], P[], 另有B2[MAXN+1][]默认初始化为0。
int CompletePack_2(int n, int c)//动态规划:F[i][j] = max(F[i-1][j], F[i][j-W[i]] + P[i])
{
 	for (int i=1; i<=n; i++)
	{
		for (int j=1; j<=c; j++) //语句1
		{
			if (j < W[i])
				B2[i][j] = B2[i-1][j];
			else  
				B2[i][j] = max(B2[i-1][j], B2[i][j-W[i]] + P[i]); //语句2
		}
	}
	
	return B2[n][c];
}

问题1:语句1能否改为:for (int j=c; j>0; j--) ?为什么?
问题2:语句2能否改为:B2[i][j] = max(B2[i-1][j], B2[i-1][j-W[i]] + P[i]);?为什么?

参考答案: 
问题1:不能。因为B2[i][j] = max(B2[i-1][j], B2[i][j-W[i]] + P[i]);即B2[i][j]的值可能由B2[i][j-W[i]]来决定,也就是说第i行的某个元素可能由同一行中列坐标j较小的元素来决定,故应该先求出第i行列坐标j较小的元素,再求j较大的元素,则循环变量j要递增。
问题2:不能。因为第i件物品可以装无限次,故做出本次选择之前,背包中可能已经装有第i件物品了,即使本次选择不装第i件物品,也是在给定i个物品的条件下做出的选择,而不是在给定i-1个物品的条件下做出的选择。
	若把语句2改为B2[i][j] = max(B2[i-1][j], B2[i-1][j-W[i]] + P[i]);则变成0-1背包问题了。

算法3:动态规划:使用2个一维数组存储记录,需要用到全局变量W[], P[], 另有pre[]和cur[]均初始化为0。
int CompletePack_3(int n, int c)//优化的动态规划算法,用2个一维数组代替二维数组 
{ 
 	for (int i=1; i<=n; i++)
	{  
		for (int j=1; j<=c; j++)
		{
			if (j < W[i] || pre[j] > cur[j-W[i]] + P[i])
				cur[j] =  //语句1
			else
				cur[j] =   //语句2
		}
		for (int j=0; j<=c; j++)
		{
			pre[j] = cur[j]; 
		}
	}
	
	return cur[c];
}

问题1:将语句1和语句2补充完整。

参考答案: 
问题1:语句1:cur[j] = pre[j];
        语句2:cur[j] = cur[j-W[i]] + P[i];

算法4:优化的动态规划算法,一维数组存储记录,需要用到全局变量W[], P[], 另有B4[]初始化为0。
int CompletePack_4(int n, int c)//进一步优化的动态规划算法,用1个一维数组代替二维数组
{
 	for (int i=1; i<=n; i++) 
	{ 
		for (int j=W[i]; j<=c; j++)  //语句1
		{ 
			if (B4[j] < B4[j-W[i]] + P[i])
				B4[j] =  //语句2
		}
	}
	
	return B4[c];
}

问题1:语句1能否改为:for (int j=c; j>=W[i]]; j--)?为什么?
问题2:将语句2补充完整。

参考答案:
问题1:不能。我们与用二维数组记录结果的算法做比较:
在二维数组算法中B2[i][j] = max(B2[i-1][j], B2[i][j-W[i]] + P[i]); 即B2[i][j]的值可能由B2[i][j-W[i]]来决定,也就是说第i行的某个元素可能由同一行中列坐标j较小的元素来决定,故应该先求出第i行列坐标j较小的元素,再求j较大的元素,则循环变量j要递增。
若我们用一维数组B4[j]代替B2[i][j],则只记录了列坐标j,未记录行坐标i,在同一行中,必须先求出列坐标j较小的元素,再求j较大的元素。这样先改变的是下标j较小的元素,再利用它来求j较大的元素。故在内层循环中,应该让循环变量j的值从小到大递增。
说明:完全背包问题与0-1背包问题的一维数组算法除了内层循环变量j的值变化方向相反以外,其他部分代码完全相同。但就是这点不同,造成逻辑上的巨大差异。
问题2:语句2:B4[j] = B4[j-W[i]] + P[i];

拓展练习:通过算法2的CompletePack_2()函数,我们用二维数组B2[][]记录了各种解的信息,现在请你根据B2[][]记录的信息,设计一个递归函数void Show(int i, int j);//i和j分别表示正在处理的第i个物品和此时背包的剩余容量。
输出物品装载情况,按照编号顺序,递归输出装入背包的物品信息(编号,数量,重量,价值)。

参考答案:
void Show(int i, int j)  //i和j分别表示正在处理的第i个物品和此时背包的剩余容量
{
	if (j == 0 || i == 0)
		return;
	
	if (B2[i][j] == B2[i-1][j]) 
	{
		Show(i-1, j); //未装载物品i
	}
	else
	{
		for (int k=j/W[i]; k>0; k--)
		{
			if (B2[i][j] == B2[i-1][j-k*W[i]] + k*P[i]) //装载了k个物品i 
			{
				Show(i-1, j-k*W[i]);
				cout << i << ": " << k << " " << W[i] << " " << P[i] << endl;
				return;
			}
		}
	}
}

课后练习:
练习1: 1426_找零钱的程序
描述:现在假设你是个店员,为了方便/准确/最优的找零钱,你设计了一个程序.该程序应该实现如下功能:
第一行输入客户所给你金额
第二行输入客户消费的总金额
第三行输出应找的总零钱是多少
第四行输出各种面额的张数(总金额之和要与第三行的数相等,并且要求货币总张数是最少的方案输出)
注:为了简单,假设上述中的金额都是整数,现规定金额的面值为100,50,20,10,5,1元.并且假定客户的金额总是大于所需支付的总金额.
数据类型用int整数表示.
输入描述 Input Description
第一行输入一个整数(表示客户所付的金额),如100
第二行输入一个整数(表示商品的总计金额),如25
输出描述 Output Description
第一行输出 应找的零钱,如75
第二行输出 金额面值1*张数1+金额面值2+张数2+....+金额面值N*张数N=总零钱数:.(面值较大的零钱优先排在前面,如50元比20元大,应排在前面)
样例输入 Sample Input
样例输入1:
100
25
样例输入2:
95
2
样例输出 Sample Output
样例输出1:(面值较大的零钱优先排在前面)
75
50*1+20*1+5*1=75
样例输出2:(面值较大的零钱优先排在前面)
93
50*1+20*2+1*3=93

练习2:整数划分问题 
描述: 所谓整数划分,是指把一个正整数n写成如下形式: 
n=m1+m2+...+mi; (其中mi为正整数,并且1 <= mi <= n),则{m1,m2,...,mi}为n的一个划分。 
如果{m1,m2,...,mi}中的最大值不超过m,即max(m1,m2,...,mi)<=m,则称它属于n的一个m划分。 
这里我们记n的m划分的个数为f(n,m); 
例如当n=4时,有5个划分,{4},{3,1},{2,2},{2,1,1},{1,1,1,1}; 
注意4=1+3 和 4=3+1被认为是同一个划分。 
该问题是求出n的所有划分个数,即f(n, n)。

提示:这是一个典型的完全背包问题,只不过不是求最优解,而是求所有可能的组合,故需要累计所有的组合。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值