背包问题的空间优化
01背包
题目描述
有 \(n\) 个重量和价值分别为 \(w_i\),\(v_i\) 的物品。从这些物品中挑选出总重量不超过 \(W\) 的物品,求所有挑选方案中价值总和的最大值。
数据范围:
\(1\le n\le100\)
\(1\le w_i,v_i\le100\)
\(1\le W\le10000\)
优化前
- 递推式:
\[\begin{split}
dp[0][j]&=0 \\
dp[i + 1][j]&=\begin{cases}
dp[i][j]&,j<w_i \\
max\{dp[i][j],dp[i][j-w_i]+v_i\}&,j\ge w_i
\end{cases}
\end{split}
\]
- 代码:
// 输入
int n, W; // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX]; // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[MAX][MAX] // dp数组,与记忆化数组一样,必须足够大
void solve(void)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j <= W; j++)
{
if (j < w[i])
dp[i + 1][j] = dp[i][j];
else
dp[i + 1][j] = max(dp[i][j], dp[i][j - w[i]] + v[i]);
}
}
printf("%d\n", dp[n][W]);
}
优化一:二维数组
由 \(dp[i + 1][j]=max\{dp[i][j],dp[i][j-w_i]+v_i\}\),我们可以发现,虽然 \(dp[MAX][MAX]\) 有 \(MAX\) 行,但是每次只访问 \(i\) 和 \(i+1\)行。因此,我们只要用一个 \(dp[2][MAX]\) 就能实现原来的功能。
// 输入
int n, W; // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX]; // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[2][MAX] // dp数组,与记忆化数组一样,必须足够大
void solve(void)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j <= W; j++)
{
if (j < w[i])
dp[(i + 1) & 1][j] = dp[i & 1][j];
else
dp[(i + 1) & 1][j] = max(dp[i & 1][j], dp[i & 1][j - w[i]] + v[i]);
}
}
printf("%d\n", dp[n & 1][W]);
}
&
按位与是一个双目操作符,对数字的二进制位进行操作。规则是:两个数字相对应的二进制位相同,运算结果为1;不同,则为0。- 由十进制与二进制的转换规则可知,从右往左看,偶数的第一个二进制为0;奇数的第一个二进制位为1。
所以,我们可以得到:
\[i\&1=\begin{cases}
1&, i=2\ast k+1\\
0&,i=2\ast k
\end{cases}
k=0,1,2...
\]
优化二:一维数组
动态规划的核心是先记录,再访问。如果访问过后,数据就不要再需要了,那么覆盖这条记录也不会对结果造成影响。因此,我们只使用一个一维数组 \(dp[MAX]\) 也能达到目的。
我们再来看递推式:
\[\begin{split}
dp[0][j]&=0 \\
dp[i + 1][j]&=\begin{cases}
dp[i][j]&,j<w_i \\
max\{dp[i][j],dp[i][j-w_i]+v_i\}&,j\ge w_i
\end{cases}
\end{split}
\]
- 当 \(j<w_i\) 时,从二维的视角来看,数据从 \(dp[i][j]\) 拷贝到 \(dp[i+1][j]\);切换到一维,数据其实没有发生变化,也就是说不需要对原来的数据进行操作。
- 当 \(j\ge w_i\) 时,从二维的视角来看,计算 \(dp[i+1][j]\) 需要访问 \(dp[i][j]\) 和 \(dp[i][j-w_i]\)。再到一维,就可以发现,需要的数据是总是在该位置的左半部分(要计算第 \(j\) 列,需要访问第 \(j\) 列和第 \(j-w_i\) 列的数据)。也就是说,右半部分的数据是可以覆盖的)。
原来的代码是
for (int j = 0; j <= W; j++)
因为是从左向右的循环,所以肯定影响后来的计算。若改为从右往左计算,则能避免这样的问题。
\[dp[j]=max\{dp[j],dp[j-w_i]+v_i\},j\ge w_i
\]
// 输入
int n, W; // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX]; // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[MAX] // dp数组,与记忆化数组一样,必须足够大
void solve(void)
{
for (int i = 0; i < n; i++)
{
for (int j = W; j >= w[i]; j--)
{
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
printf("%d\n", dp[W]);
}
完全背包
题目描述
有 \(n\) 个重量和价值分别为 \(w_i\),\(v_i\) 的物品。从这些物品中挑选出总重量不超过 \(W\) 的物品,求所有挑选方案中价值总和的最大值。在这里,每种物品可以挑选任意多件。
数据范围:
\(1\le n\le100\)
\(1\le w_i,v_i\le100\)
\(1\le W\le10000\)
优化前
- 递推式
\[\begin{split}
dp[0][j]&=0 \\
dp[i + 1][j]&=\begin{cases}
dp[i][j]&,j<w_i \\
max\{dp[i][j],dp[i+1][j-w_i]+v_i\}&,j\ge w_i
\end{cases}
\end{split}
\]
- 代码
// 输入
int n, W; // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX]; // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[MAX][MAX] // dp数组,与记忆化数组一样,必须足够大
void solve(void)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j <= W; j++)
{
if (j < w[i])
dp[i + 1][j] = dp[i][j];
else
dp[i + 1][j] = max(dp[i][j], dp[i + 1][j - w[i]] + v[i]); // 与01背包唯一的不同
}
}
printf("%d\n", dp[n][W]);
}
优化一:二维数组
// 输入
int n, W; // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX]; // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[2][MAX] // dp数组,与记忆化数组一样,必须足够大
void solve(void)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j <= W; j++)
{
if (j < w[i])
dp[(i + 1) & 1][j] = dp[i & 1][j];
else
dp[(i + 1) & 1][j] = max(dp[i & 1][j], dp[(i + 1) & 1][j - w[i]] + v[i]); // 与01背包唯一的不同
}
}
printf("%d\n", dp[n & 1][W]);
}
优化二:一维数组
- 当 \(j<w_i\) 时,与01背包中的分析一样。
- 当 \(j\ge w_i\) 时,与01背包唯一的不同是,其中一个数据由 \(dp[i][j-w_i]\) 变成了 \(dp[i+1][j-w_i]\)。虽然都是访问计算点左半部分的数据,但是,需要注意的是,访问的是新数据(\(i+1\)),不是老数据(\(i\))。因此,第二个循环必须是从左往右的。
\[dp[j]=max\{dp[j],dp[j-w_i]+v_i\},j\ge w_i
\]
// 输入
int n, W; // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX]; // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[MAX] // dp数组,与记忆化数组一样,必须足够大
void solve(void)
{
for (int i = 0; i < n; i++)
{
for (int j = w[i]; j <= W; j++)
{
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
printf("%d\n", dp[W]);
}
与01背包比较,通过该方法优化后,二者代码的区别仅在第二个循环的方向:
- 01背包:
for (int j = W; j >= w[i]; j--) \\ 从右往左
- 完全背包:
for (int j = w[i]; j <= W; j++) \\ 从左往右
完
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具