背包问题总结
01背包 | 完全背包 | 多重背包 | |
要求填满 | 略微变动 | 略微变动 | 转化01背包 |
不要求填满 | 基础 | 基础 | 转化01背包 |
问题模型:
有若干种物品,每种物品有特定的价值和所需要的空间。
要求,在不超过空间的情况下求出最大的价值总和(注意不是方法数,如果说是方法数可用母函数解决)。
核心模板:
for (int i = 1; i <= n; i++)//这里的n是物品的种类
{
scanf("%d %d", &j, &f);//这里f读取的是所需要的空间,也就是消耗的资源,j读取的是价值,这里边读边更新
for (int k = m; k >= f; k--)//此处k是放置每一种物品时候的各个大小背包的空间
{
dp[k] = max(dp[k], dp[k - f] + j);//一维数组最强,读取位置左侧为i - 1右侧为i,从左到右是完全背包,从右到左是01背包
}
}
模板理解:
1、一维数组是二维数组的空间压缩版本,本质上是相同的。
2、先看模板。
第一步:我们针对每一种物品进行动态规划。
第二步:我们读取这一种物品的数据情况,价值和数量。
注意:如果不是一个一个输的,我们可以先存在两个一维数组当中,再调用。
第三步:我们针对这一种物品,考虑目标背包的子背包的最优方案,动态规划的思想。
第四步:状态转移方程,也就是递推公式。
观察递推。
a.其中dp[k] = max(dp[k],....),是在利用各个dp[k]存储各个可能值的最大最大值,在dfs的路 径搜索的时候也是经常使用的
b.我们要理解,dp[k - f] + j,我们假设在此处的dp[k]之前的问题我们都已经解决了, d[k - f]的含义就是,当背包容量为k - f的时候,之前放置的所有种类物品的最优背包方案,相当于我们在向容量k的背包放置的时候,挪出来f的空间给这个物品,此处未更新前的dp[k]是没有考虑这个物品的最好背包,现在我们考虑要不要放这个物品,如果放这个物品我们最优方案是什么,显然空间越大价值肯定不会减小,我们希望在之前的最大的背包里放入这个物品,因此我们选刚刚好可以放入这个物品的dp[k - f]背包,这样,我们就成功更新了这个背包,转化为了考虑放入了这个物品的最好背包,从而可以作为下一步的历史数据。
3、初始化的状态,不要求填满的背包,初始化为0。
为什么呢?因为我们在考虑完第零种物品后,最好背包都是价值为0;
但是,要求填满的背包,初始化为负无穷。
为什么呢?因为我们一定要求,必须从空间为0的背包,一步一个脚印的堆叠到目标容量,不可以踩着虚空。
memset(dp, -0x3f, sizeof(dp))如此操作,并且把dp[0] = 0,因为空间为0的背包是基点。
4、关于数组滚动的方法,外层没关系,因为所有的物品都要考虑,先考虑第一种和先考虑第n种本质上是没有区别的,结果是一样的。
但是, 内层关系到背包的类型。
dp[k]
k |
数组在滚动,滚动后的是更新后的地方,但是我们考虑背包容量的时候是考虑小的地方放向是一定的,我们参考的都是左侧的dp值。
当从左往右的时候,我们考虑挪出容量后的背包的话,我们参考的是已经考虑过该种物品的历史数据。
这样,我们就造成了一个物品数量不限的结果,也就是完全背包。因为就算我们已经考虑过了我们还可以再次去考虑它,可以多次放置,不限次数。
当从右往左的时候,我们考虑挪出容量后背包的话,我们参考的是还没有考虑过这种物品的历史数据。
这样,我们就造成了一个数量唯一的局面,也就是01背包。因为我们最多只考虑一次,甚至不考虑放置该物品。
5、关于多重背包,我们的方法是拆解背包。
如何是有效的拆解方法呢?首先我们不能消除其在最终背包里的数目的可能性,而且要足够简便。
你可以直接单个单个拆,但是如果数组量大的时候就会TLE。
一般常用二进制拆解的方法。拆成1 2 4 8 ....个一点余数
书写方法:
int k = 1;//这是二进制拆包的开端
int cnt = 0;//这是用来统计包的数目,用来放到最后考虑的物品的种类数
while (n >= k)//n是要分解的数
{
n = n - k;//拆去一个数
v[++cnt] = k * a;//新的物品的体积和价值
w[cnt] = k * b;
k = k * 2;//翻倍
}
if (n)//如果有剩余的话单独成包
{
v[++cnt] = n * a;
w[cnt] = n * b;
}
二进制拆包的神奇之处在于可以通过组合可以搭配出0~n的所有数,没有因为随意打包而导致某些可能性的缺失,而且能够大大减少后续判断的次数。
如有缺漏还请多多指教。