今天是放寒假的重拾算法的第二天,渡过恐怖期末周后,这段期间都没怎么学习编程了,感觉自己又变成新兵蛋子了,趁寒假时间多,就多学学吧,这是寒假第一篇,争取寒假能写个十篇吧,话不多说上干货!!!
多重背包(Ⅰ)---无优化版本
以今天写的代码展开详细描述与解释,并附上题目

cpp
const int N = 110;
int x[N], w[N], v[N];//个数,重量,价值
int f[N][N];
int main()
{
int m, n;
cin >> m >> n;
for (int i = 1; i <= m; i++)
{
cin >> x[i] >> w[i] >> v[i];
}
for (int i = 1; i <= m; i++)//这个是种类
{
for (int j = n; j >= 0; j--)//这个是容量
{
for (int k = 0; k <= x[i] && k * w[i] <= j; k++)//k=0意为可以不选
{
f[i][j] = max(f[i][j], f[i - 1][j - k * w[i]] + k * v[i]);
}
}
}
cout << f[m][n] << endl;
return 0;
}
这是一道在洛谷上写的题目,所以使用全局的变量更加方便,根据输入要求,我直接把数组设置的足够大,那么就不用考虑越界的问题
题目的主要实现逻辑在于三个for循环,理解每个循环的功能就能够理解其中的门道了
- 数组x,w,v:分别存储的是每个种类的初始数据
- 二维数组f:是用来进行动态规划的数组
- 第一层for(i)循环:是遍历一遍所有的种类
- 第二层for(j)循环:是遍历一次所有可能的容量
从右往左的原因:不同于完全背包,完全背包可以无限制重复选择,但是多重背包不行,因为多重背包每个物品并没有无限个,是有限的;如果是从左往右遍历,那么就变成了完全背包,不符合题意;所以,从右往左遍历,就可以不打破多重背包有数量限制的约束
- 第三层for(k)循环:是遍历一次每个种类所可以选的数量,k需要满足条件(k <= x[i] && k * w[i] <= j),否则就不是符合条件的情况,这样可以直接剪枝,减少时间复杂度,k可以从0开始,因为可以选择不选
- 最后输出的f[m][n]就是所需结果
多重背包(Ⅱ)---二进制优化版本
这个两个版本的题目都一样,就不重复展示了,这里直接展示代码
cpp
const int N = 110 * 5;//一个物品最多20个,但是可以分成五堆
//(就是五种虚拟物品,并且有100个种类,最多需要的位置是500个位置)
//(这个是在加入x都是20的情况下)
int w[N], v[N], pos = 0;//重量,价值
int f[N];
int main()
{
int n, T;
cin >> n >> T;
for (int i = 1; i <= n; i++)
{
int x, y, z;
cin >> x >> y >> z;
int t = 1;
while (x >= t)
{
pos++;
w[pos] = t * y;
v[pos] = t * z;
x -= t;
t *= 2;
}
if (x)
{
pos++;
w[pos] = x * y;
v[pos] = x * z;
}
}
//类似01背包,此时每个虚拟物品只有一个
for (int i = 1; i <= pos; i++)//遍历每个虚拟物品的重量
{
for (int j = T; j >= w[i]; j--)//寻找符合条件的重量,并计算最大价值
{
f[j] = max(f[j], f[j - w[i]] + v[i]);
}
}
cout << f[T] << endl;
return 0;
}
题目的主要实现逻辑在于理解二进制优化,和模拟01背包动态规划
- 此处我们要先理解这个原理---二进制优化
一个数例如7,我们二进制形式是0111,即1+2+4;那么在这里我们就可以把一个种类的数量划分成三堆,第一堆有一个,第二堆有两个,第三堆有四个;那么因为每一个的重量和价值都一样,那么我们可以把两个和四个的集合看成一个整体,设这个种类的重量为w,价值为v,那么两个为一个整体的话,它此时的重量为2w,质量为2v,以此类推;就是把每一个集合看成一个新的个体计算,减少了时间复杂度,可以转化成01背包问题;再有特殊,有余数的情况,例:20,它可以写成1+2+4+8+5;因为5没办法用单独一个2的次方表示,可以独立形成一个集合,再进行计算统计,🆗原理就是差不多这些,还不理解的话,可以尝试问问豆包举例举例,方便理解
- 变量x,y,z:是题目的源数据
- 变量pos:是用来统计把原来的种类转化成虚拟物品后有多少的个数统计
- 常量N:是用来开数组空间的,为什么是550?因为题目说了,每个种类的物品最多有20个,然后20又最多划分成5堆,并且一共有100个种类,以防数组越界我初始化数组都是110个,所以最终由5*110个空间(理解了这里,差不多就知道,题目的实现逻辑了)
- 第一个while循环:就是统计每个种类有几个集合,并且把每个集合对应的重量和价值存入w与v数组
- 第二个while循环:在原理那一块,会有像20这种数字没办法完全变成由2的次数组成的数字,那么这个循环的作用就是统计余数集合的重量和价值
- 双层for循环:就是模拟01背包问题,因为我们把源数据划分成了一个个的虚拟集合,相当于每个虚拟集合只有一个,就可以使用01背包的思维进行解答
- 注:这个二进制优化不能用于解决求方案数问题,只能用于解决求最值问题
因为,我们集合里的物品都是同一种,例如9这个数字,可以分为1+2+4+2;如果我们 要找到符合重量为3的搭配方案(假设每个物品的重量为1),就可以把1和两个2分别搭 配,但是其实都是同一种情况,会重复统计
哇!酣畅淋漓的码字,希望读者可以看懂
多重背包(Ⅲ)---求方案数问题
以今天写的代码展开详细描述与解释,并附上题目

cpp
const int N = 1e6 + 7;
const int M = 110;
int a[M];
int dp[M][M];
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
dp[0][0] = 1;
for (int i = 1; i <= n; i++)
{
for (int j = m; j >= 0; j--)
{
for (int k = 0; k <= a[i] && k <= j; k++)
{
dp[i][j] = (dp[i][j] + dp[i - 1][j - k]) % N;
}
}
}
cout << dp[n][m] << endl;
return 0;
}
这是一道在洛谷上写的题目,所以使用全局的变量更加方便,根据输入要求,我直接把数组设置的足够大,那么就不用考虑越界的问题
题目的主要实现逻辑在于三个for循环,理解每个循环的功能就能够理解其中的门道了
- 数组a:用于存储标记每个种类不能摆放超过的数量
- 第一层for(i)循环:遍历一遍所有的种类
- 第二层for(j)循环:遍历一遍所有可能容量数
- 第三层for(k)循环:遍历这个种类所拥有的个数,但是k要满足条件(k <= a[i] && k <= j),否则是不符合标准的;因为方案数可能很大,在每次计算时,我们要边求和,边取模,就可以保证最终的结果准确
- 最终返回dp[n][m]的数值
优化版本
cpp
const int N = 1e6 + 7;
const int M = 110;
int a[M];
int dp[M];
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
dp[0] = 1;
for (int i = 1; i <= n; i++)
{
for (int j = m; j >= 0; j--)
{
for (int k = 1; k <= a[i] && k <= j; k++)
{
dp[j] = (dp[j] + dp[j - k]) % N;
}
}
}
cout << dp[m] << endl;
return 0;
}
这个是空间优化的版本
空间优化时,我们不仅仅要修改数组的维数,还要更改k的起始遍历值,若k还是从0开始遍历,那么就是导致重复统计dp[j]的情况,导致结果错误
