背包五讲
01背包
题面
有 \(N\) 件物品和一个容量是 \(m\) 的背包。每件物品只能使用一次。第 \(i\) 件物品的体积是 \(v_i\),价值是 \(w_i\)。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
思路
定义 \(dp_{i,j}\) 为前 \(i\) 个物品,体积为 \(j\) 的最大价值。
由此我们可以讨论取或不取,推出转移方程。
由于对 \(dp_{i}\) 的影响只有 \(dp_{i-1}\),那我们不妨去掉第一维,直接用 \(dp_i\) 表示体积为 \(j\) 时的最大价值,所以方程为:
需要注意的是,枚举时需要倒序枚举,才能保证物品只能使用一次,顺序则为完全背包做法。
for(int i=1;i<=n;i++)
{
for(int j=m;j>=w[i];j--)//倒序枚举
{
dp[j]=max(dp[j],dp[j-w[i]]);
}
}
完全背包
题面
有 \(N\) 件物品和一个容量是 \(m\) 的背包。每件物品可以使用无限次。第 \(i\) 件物品的体积是 \(v_i\),价值是 \(w_i\)。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
思路
根据 01 背包,我们不难想出正序枚举即可实现物品使用多次。用 \(dp_i\) 表示体积为 \(j\) 时的最大价值,所以方程与 01 背包一模一样:
for(int i=1;i<=n;i++)
{
for(int j=w[i];j<=m;j++)//正序枚举
{
dp[j]=max(dp[j],dp[j-w[i]]);
}
}
多重背包
题面
有 \(N\) 件物品和一个容量是 \(m\) 的背包。每件物品可以使用 \(c_i\) 次。第 \(i\) 件物品的体积是 \(v_i\),价值是 \(w_i\)。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
暴力思路
容易想到,把「每个物品拆成 \(c_i\) 个只能使用一次的物品」,再套用 01 背包的模板即可。
转移方程即为
单调队列优化
我们在暴力思路上容易观察对于每一个 \(dp_{i,j}\) 都是从 \(dp_{i-1 \times w_i},dp_{i-2 \times w_i} …… dp_{i-c_i \times w_i}\) 转移而来的。他们第二维对 \(w_i\) 的余数都相同。
那我们不妨跳着计算 \(dp_{i,j}\),固定一个余数 \(r\)。
既然余数统一,形式就可以表示成 \(r+x \times v_i\),而 \(x\) 是连续的,那对于当前的 \(k\) 我们需要求的就是 \(x\) 在区间 \([k-c_i,k]\) 上 \(dp_{i-1,r+x \times w_i}+(k-x) \times v_i\) 的最大值。
固定长度的区间最大值,即可用单调队列来实现。
有一个小细节就是队列里存的是 \(i-1\) 的 dp 值。
具体细节见代码。
for(int i=1;i<=n;i++,b^=1)//滚动数组
{
for(int r=0;r<v[i];r++)//固定余数
{
while(!q.empty()) q.pop_back();//清空队列
for(int k=0;k<=m/v[i];k++)
{
while(!q.empty()&&q.front()<k-c[i]) q.pop_front();//弹队首
while(!q.empty()&&dp[b^1][r+q.back()*v[i]]+(k-q.back())*w[i]<dp[b^1][r+k*v[i]]) q.pop_back();//维护队列递减性,注意是 b^1
q.push_back(k);
dp[b][r+k*v[i]]=(dp[b^1][r+q.front()*v[i]]+(k-q.front())*w[i]);//更新答案
}
}
}
分组背包
题面
有 \(n\) 件物品和一个大小为 \(m\) 的背包,第 \(i\) 个物品的价值为 \(w_i\),体积为 \(v_i\)。同时,每个物品属于一个组,同组内最多只能选择一个物品。求背包能装载物品的最大总价值。
思路
这道题其实和 01 背包非常的相似,其实是从「在所有物品中选择一件」变成了「从当前组中选择一件」,于是就对每一组进行一次 01 背包就可以了。
注意循环顺序,才能保证答案正确性。
for (int k = 1; k <= zs; k++)//循环每一组
for (int i = m; i >= 0; i--)//循环背包容量
for (int j = 1; j <= cnt[k]; j++)//循环该组的每一个物品
if (i >= w[z[k][j]])
dp[i] = max(dp[i],dp[i - w[z[k][j]]] + c[z[k][j]]);//像 01 背包一样状态转移
求字典序最小方案
题面
有 \(N\) 件物品和一个容量是 \(m\) 的背包。每件物品只能使用一次。
第 \(i\) 件物品的体积是 \(v_i\),价值是 \(w_i\)。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 \(1……N\)。
思路
在背包求解的基础上,新建一个数组记录是「从哪一个转移过来」的就行了,注意不能压维,不然无法判断具体是从哪一个转移过来的。
要倒着枚举,保证字典序最小。
for(int i=n;i>=1;i--)
{
for(int j=0;j<v[i];j++)
{
dp[i][j]=dp[i+1][j];
path[i][j]=path[i+1][j];
}
for(int j=v[i];j<=m;j++)
{
if(dp[i+1][j]>dp[i+1][j-v[i]]+w[i])//注意如果相等尽可能取当前数
{
dp[i][j]=dp[i+1][j];
path[i][j]=path[i+1][j];
}
else//因为倒着枚举,当前数字典序更小
{
dp[i][j]=dp[i+1][j-v[i]]+w[i];
path[i][j]=i;
}
}
cout<<dp[1][m]<<" ";
int x=path[1][m];
s.push(x);
m-=v[x];
do{
x=path[x+1][m];
m-=v[x];
s.push(x);
}while(path[x+1][m]);
cout<<s.size()<<"\n";
while(!s.empty())
{
cout<<s.front()<<" ";
s.pop();
}

浙公网安备 33010602011771号