背包五讲

01背包

题面

\(N\) 件物品和一个容量是 \(m\) 的背包。每件物品只能使用一次。第 \(i\) 件物品的体积是 \(v_i\),价值是 \(w_i\)。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

思路

定义 \(dp_{i,j}\) 为前 \(i\) 个物品,体积为 \(j\) 的最大价值。

由此我们可以讨论取或不取,推出转移方程。

\[dp_{i,j} = max(dp_{i-1,j-v_i} + w_i,dp_{i-1,j}) \]

由于对 \(dp_{i}\) 的影响只有 \(dp_{i-1}\),那我们不妨去掉第一维,直接用 \(dp_i\) 表示体积为 \(j\) 时的最大价值,所以方程为:

\[dp_{i} = max(dp_{i-v_i} + w_i,dp_{i}) \]

需要注意的是,枚举时需要倒序枚举,才能保证物品只能使用一次,顺序则为完全背包做法。

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 背包一模一样:

\[dp_{i} = max(dp_{i-v_i} + w_i,dp_{i}) \]

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} = max(dp_{i,j-k \times w_i}+k \times v_i)(0 \le k \le c_i) \]

单调队列优化

我们在暴力思路上容易观察对于每一个 \(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();
	}
posted @ 2026-03-28 09:20  Azarole  阅读(4)  评论(0)    收藏  举报