/*
* 有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
多重背包问题的常规算法和二进制拆分法
最新推荐文章于 2022-12-02 11:21:24 发布
![](https://img-home.csdnimg.cn/images/20240711042549.png)