一、普通背包问题
问题描述:
给定 n 件物品,物品的重量和价值分别为w[i]和v[i]。现挑选物品放入背包中,假定背包能承受的最大重量为 m,问应该如何选择装入背包中的物品,使得装入背包中物品的总价值最大?
即只需要决定放不放这个物品,不涉及放多少的问题(物品只有一个)。
直接想法:
暴力枚举(时间复杂度O(2ⁿ))
即排列组合, 枚举每一个物品是否在背包里
改进想法:
剪枝法(1.跳过超重的部分 2.相同重量且有更优解时,及时剪枝)
在将上述爆搜方法的缺点克服后, 我们已经窥得动态规划的一点端倪。或者说,动态规划本质上是一种优化的爆搜
进阶想法:
动态规划
即f数组是一个集合, 其存储的值是集合中唯一有意义的情况,时间复杂度O(nm),明显优于暴力
关于初始化,因为后面有剪枝,而且下一排需要用到它的值,这样是为了使每个f [i] [c]都能有正确的值
for(int i=0;i<n;i++) //枚举每件物品
{
for(int c=0;c<=m;c++) //枚举每个重量
{
f[i][c]=f[i-1][c]; //初始化
if(w[i]>c) continue; //剪枝去超重情况
f[i][c]=max(f[i][c],f[i-1][c-w[i]]+v[i]); //取拿和不拿中的最大值
}
}
一维数组优化
有的朋友就问了,这里为什么没有初始化了? 因为这是一个一维数组,每排用到的都是当前所在的值
至于内循环顺序为什么变了,因为每个物品只有一个,如果先排前面的,就有可能出现放两次的可能
for(int i=1;i<=n;i++)
{
for(int c=m;c>=0;c--)
{
if(w[i]>c) continue;
f[c]=max(f[c],f[c-w[i]]+v[i]);
}
}
更进一步的常数优化:
改后的内层循环只循环到最小的会改变的重量的地方,避免了部分无效循环
for(int i=1;i<=n;i++)
{
sumw+=w[i];
bound=max(m-sumw,w[i]);
for(int c=m;c>=bound;c--)
{
f[c]=max(f[c],f[c-w[i]]+v[i]);
}
}
二、完全背包问题
问题描述:
给定 n 件物品,物品的重量和价值分别为w[i]和v[i],每一件物品可以取无数个。现挑选物品放入背包中,假定背包能承受的最大重量为 m,问应该如何选择装入背包中的物品,使得装入背包中物品的总价值最大?
即需要决定放不放这个物品,如果放要放多少个的问题(物品有无数个)。
直接想法:
直接在普通背包动态规划的基础上对每个物品,枚举其可能的个数(时间复杂度O(nmk))(k=m/w[i])
对于一些比较特殊的样例,可以将这个时间复杂度卡得很高
for(int i=0;i<n;i++) //枚举每件物品
{
for(int c=0;c<=m;c++) //枚举每个重量
{
f[i][c]=f[i-1][c]; //初始化
for(int k=0;k<=c/w[i];k++) //枚举拿的件数
{
f[i][c]=max(f[i][c],f[i-1][c-k*w[i]]+k*v[i]); //取拿不同件数中的最大值
}
}
}
改进想法:
二进制优化(将每次循环拿的件数从增加一件改成增加2的k次幂件直至放不下)
for(int i=1;i<=n;i++)
{
for(int a=1;a*2<m;a*=2)
{
for(int c=m;c>=w[i];c--)
{
f[c]=max(f[c],f[c-a*w[i]]+a*v[i]);
}
}
}
进阶想法:
一维数组优化:
其实不难发现,以下代码与普通背包一维数组优化只有内循环顺序有差别,因为这次需要包括放多个的情况
for(int i=1;i<=n;i++)
{
for(int c=0;c<=m;c++)
{
if(w[i]>c) continue;
f[c]=max(f[c],f[c-w[i]]+v[i]);
}
}
三、多重背包问题
问题描述:
给定 n 件物品,物品的重量和价值分别为w[i]和v[i],每一件物品对应的数量有u[i]个。现挑选物品放入背包中,假定背包能承受的最大重量为 m,问应该如何选择装入背包中的物品,使得装入背包中物品的总价值最大?
即需要决定放不放这个物品,如果放要放多少个的问题(物品有u[i]个)。
直接想法:
直接在普通背包动态规划的基础上对每个物品, 枚举其可能的个数(时间复杂度O(nmk))
对于一些比较特殊的样例,可以将这个时间复杂度卡得很高
for(int i=0;i<n;i++) //枚举每件物品
{
for(int c=0;c<=m;c++) //枚举每个重量
{
f[i][c]=f[i-1][c]; //初始化
for(int k=0;k<=u[i];k++) //枚举拿的件数
{
if(u[i]*w[i]>c) continue;
f[i][c]=max(f[i][c],f[i-1][c-k*w[i]]+k*v[i]); //取拿不同件数中的最大值
}
}
}
改进想法:
二进制优化(将每次循环拿的件数从增加一件改成增加2的k次幂件直至放不下或者不够,将剩余的再循环一次)
for(int i=1;i<=n;i++)
{
for(int a=1;a*2<m&&a*2<u[i];a*=2)
{
for(int c=m;c>=a*w[i];c--)
{
f[c]=max(f[c],f[c-a*w[i]]+a*v[i]);
}
}
if(a*2<=m) //如果放得下,就放剩余的
{
for(int c=m;c>=(m-a+1)*w[i];c--) //(m-a+1)是剩余的数量
{
f[c]=max(f[c],f[c-(m-a+1)*w[i]]+(m-a+1)*v[i]);
}
}
}
进阶想法:
单调队列优化
由于理解难度较高,以下只给出模板,具体原理可以自己去参考https://github.com/tianyicui/pack(背包九讲),原理讲的特别清晰!!!
for(int i=1;i<=n;i++)
{
if(w[i]*a[i]>m)
{
for(int c=0;c<=m;c++)
{
if(c>=w[i])
f[c]=max(f[c],f[c-w[i]]+v[i]);
}
}
else
{
k=1;amount=a[i];
while(k<amount)
{
for(int c=k*w[i];c>=0;c--)
{
if(c>=w[i])
f[c]=max(f[c],f[c-w[i]]+k*v[i]);
}
amount-=k;
k<<=1;
}
for(int c=amount*w[i];c>=0;c--)
{
f[c]=max(f[c],f[c-w[i]]+amount*v[i]);
}
}
}
by SZU jt 2022-05-19