多重背包问题的常规算法和二进制拆分法

这篇博客探讨了如何使用动态规划解决有多个限制数量的物品放入固定容量背包的问题,以最大化价值。作者首先介绍了基本的动态规划解法,然后通过二进制拆分优化方法降低了时间复杂度。博客详细解释了两种方法的实现过程,并提供了代码示例。
摘要由CSDN通过智能技术生成
/*
* 有N件物品和一个容量为V的背包。
* 第i件物品的体积是c[i],价值是w[i]。
* 第i件物品最多有a[i]个可用。
* 求将那些物品装入背包可以使得价值总和最大。
*/
#include<iostream>
#include<vector>
#include<iomanip>
#include<queue>
using namespace std;
#define choice 1
class thing
{
private:
	int volume;//体积
	int value;//价值
	int quantity;//数量
public:
	thing(int _volume=0,int _value=0,int _quantity=0) :volume(_volume),value(_value),quantity(_quantity){};
	thing(const thing& external) :volume(external.volume), value(external.value), quantity(external.quantity) {};
	thing& operator=(const thing& external) 
	{ 
		if (this == &external)return *this;
		this->volume = external.volume;
		this->value = external.value;
		this->quantity = external.quantity;
		return *this;
	}
	inline const int Volume()const { return volume; }
	inline const int Value()const { return value; }
	inline const int Quantity()const{ return quantity; }
	~thing(){}
};
const vector<thing> a = {
	{2,3,2},
	{2,2,5 },
	{1,1,10},
	{3,4,1 },
	{4,5,2}
};
const int V = 15;//总体积
#if choice==0
/*
* 思路:
* 创建二维数组f[i][j]表示从前i件物品中选取的已占用j空间的背包所得到的最大价值,1<=i<=N,0<=j<=V
* 对于第i件物品的个数从0依次枚举,一直到a[i],它所占的容量为k*c[i],0<=k<=a[i]。
* 这里k*c[i]始终在背包可容纳的范围内,因此可以得到0<=k*c[i]<=V-j。
* k=0时,没有空间消耗,所以用掉的空间跟选前i-1件物品一样,即f[i][j]=f[i-1][j];
* 0<k<=a[i]时,消耗了k*c[i]的空间,因此在选前i-1件物品时,留有的空间应该比现在剩下的空间至少多k*c[i],
* 现在留有的空间是V-j,选了k个第i个物品,因此原来的剩余空间为(V-j)+k*c[i],所以原来占用了j-k*c[i]的空间。
* 可以得到递推关系式f[i][j]=min(f[i-1][j-k*c[i]]+k*w[i]),1<=k<=a[i],0<=k*c[i]<=V-j。
* k=0时,也符合这个递推关系式,所以[i][j]=max(f[i-1][j-k*c[i]]+k*w[i]),0<=k<=a[i],0<=k*c[i]<=V-j。
* 与01背包问题,完全背包问题的不同之处,这个递推式不能进一步优化。j-k*c[i]中有两个变量,所以可以来源于第i-1行的任意一个记录
* 答案是f[N][V]
*/

void show(int** r, int i, int j);
int main()
{
	int N = a.size();
	int** f = new int* [N+1];
	int** Rec = new int* [N + 1];//追踪数组
	for (int i = 0; i <= N; i++)
	{
		f[i] = new int[V + 1]{ 0 };
		Rec[i] = new int[V + 1]{ 0 };
	}
		

	for (int i = 1; i <= N; i++)
	{
		for (int j = 1; j <= V; j++)
		{
			for (int k = 0; k <= a[i - 1].Quantity(); k++)
			{
				if (j - k * a[i - 1].Volume() < 0)//超出最大容量
					break;
				if (f[i - 1][j - k * a[i - 1].Volume()] + k * a[i - 1].Value() >= f[i][j])//优先选取价值大的,所以加等号
				{
					f[i][j] =f[i - 1][j - k * a[i - 1].Volume()] + k * a[i - 1].Value();
					Rec[i][j] = k;//记录取了多少件物品a[i-1]
				}	
			}
		}
	}

	cout << "最大价值为: "<<f[N][V] << endl;
	show(Rec, N, V);

	for (int i = 0; i <= N; i++)
	{
		delete[] f[i];
		delete[] Rec[i];
	}
		
	delete[] f;
	delete[] Rec;
	return 0;
}

void show(int** r, int i, int j)
{
	if (i == 0)
		return;
	show(r, i-1,j- r[i][j] * a[i - 1].Volume());
	if (r[i][j] != 0)
		cout << "第" << i << "件物品" << "选择了" << r[i][j] << "件" << endl;
}
#elif choice==1
/*
* 上面的常规做法显然时间复杂度和空间复杂度都非常高,如何改进?
* 参考别的大神,可以采用二进制拆分优化:
* 例:
*	一个物品最多有15个可用,15的二进制是1111,可以拆分成4份,分别代表8个物品、4个物品、2个物品、1个物品。
*	0~15中的任意一个都可用这些数字组合(每一位上都有最多一个可以选,即取0或1,多重背包问题就转换成了01背包问题,自然可以表示对应的十进制数字)。
*	但是如果把12表示成二进制是1100,右边两位是没有可选的,只能表示0,4,8,12,想表示0~12中的其他数字就不行了。
*	因此对于任何的a[i],我们都应该先拆成1,2,4,……,2^k直到剩下的数不能拆为止。
*	12可以拆成1,2,4,5,(即二进制的111和十进制的5),二进制的111可以表示[0,7]中的任何数字,加上5后可以表示[5,12]中的任何数,两个区间取并集,
*	即可得到[0,12]。
*/
typedef struct MyStruct
{
	int num;
	bool End;//结尾标志
}MyStruct;
vector<MyStruct> new_quantity;//二进制拆封存储,最后一个元素为无法转换的十进制数字
void show(int** r, int i, int j,int k,int* sum);
int main()
{
	
	for (int i = 0; i < (int)a.size(); i++)
	{
		int temp = a[i].Quantity();
		int n = 1;
		while (temp >= n)
		{
			new_quantity.push_back({ 1,false });
			temp -= n;
			n <<= 1;
		}
		new_quantity.push_back({ temp,true });
	}

	int length = (int)new_quantity.size();
	int** f = new int*[length + 1];
	int** Rec = new int* [length + 1];
	int* sum = new int[(int)a.size()]{0};//记录选取物品总数
	for (int i = 0; i <= length; i++)
	{
		f[i] = new int[V + 1]{ 0 };
		Rec[i] = new int[V + 1]{ 0 };
	}
		

	int k = 0,n=1;
	for (int i = 1; i <= length; i++)
	{
		for (int j = 1; j <= V; j++)
		{
			if (new_quantity[i-1].End == false)//不是结尾的数字
				/*选择第i件物品,则占用a[k].Volume()*n的体积,
				获得了a[k].Value()*n的价值。显然0<=a[k].Volume()*n<=V-j,
				因此原先剩余体积为(V-j)+a[k].Volume()*n,原先已占有
				体积为j-a[k].Volume()*n。
				如果不选择第i件物品,则应该与选择前i-1件物品价值相同,即f[i-1][j]。
				所以递推关系式为f[i][j]=max(f[i-1][j], f[i-1][j-a[k].Volume()*n]+a[k].Value()*n)*/
			{
				if (j < n * a[k].Volume())//超出最大容量,保持原来的数据不变
					f[i][j] = f[i - 1][j];
				else
				{
					if (f[i - 1][j - n * a[k].Volume()] + n * a[k].Value() >= f[i - 1][j])
					{
						f[i][j] = f[i - 1][j - n * a[k].Volume()] + n * a[k].Value();
						Rec[i][j] = n;
					}
					else
						f[i][j] = f[i - 1][j];
				}
					
			}
			else//是结尾数字
				/*选择第i件物品,则占用a[k].Volume()*new_quantity[i].num的体积,
				获得了a[k].Value()*new_quantity[i].num的价值。显然0<=a[k].Volume()*new_quantity[i].num<=V-j,
				因此原先剩余体积为(V-j)+a[k].Volume()*new_quantity[i].num,原先已占有
				体积为j-a[k].Volume()*new_quantity[i].num。
				如果不选择第i件物品,则应该与选择前i-1件物品价值相同,即f[i-1][j]。
				所以递推关系式为f[i][j]=max(f[i-1][j], f[i-1][j-a[k].Volume()*new_quantity[i].num]+a[k].Value()*new_quantity[i].num)*/
			{
				if (j < new_quantity[i-1].num * a[k].Volume())//超出最大容量,保持原来的数据不变
					f[i][j] = f[i - 1][j];
				else
				{
					if (f[i - 1][j - new_quantity[i - 1].num * a[k].Volume()] + new_quantity[i - 1].num * a[k].Value() >= f[i - 1][j])
					{
						f[i][j] = f[i - 1][j - new_quantity[i - 1].num * a[k].Volume()] + new_quantity[i - 1].num * a[k].Value();
						Rec[i][j] = new_quantity[i - 1].num;
					}
					else
						f[i][j] = f[i - 1][j];
				}
			}
		}
		if (new_quantity[i-1].End == true)//发现结尾数字
		{
			k++;//找到下一个物品
			n = 1;
		}
		else
			n <<= 1;
	}


	cout << "最大价值为: " << f[length][V] << endl;
	show(Rec, length, V, (int)a.size(),sum);
	for (int i = 0; i <= length; i++)
	{
		delete[] f[i];
		delete[] Rec[i];
	}
	delete[] f;
	delete[] Rec;
	delete[] sum;
	/*虽然空间换时间,但这个并不是最优的方案(最优方案并不能按照我这个思路来),
	  空间复杂度理应可以继续降低,将二维表变成一维表,追踪选取了哪些物品仍然需要一张二维表*/
	return 0;
}
void show(int** r, int i, int j, int k, int* sum)
{
	if (i == 0)
		return;
	if (new_quantity[i-1].End == true)
		k--;
	sum[k] += r[i][j];
	show(r,i - 1, j - r[i][j] * a[k].Volume(), k,sum);
	if (sum[k] != 0 && new_quantity[i-1].End==true)
		cout<< "第" << k+1 << "件物品" << "选择了" << sum[k] << "件" << endl;
}
#endif
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值