01背包问题
题目描述
有 \(n\) 个重量和价值分别为 \(w_i\),\(v_i\) 的物品。从这些物品中挑选出总重量不超过 \(W\) 的物品,求所有挑选方案中价值总和的最大值。
数据范围:
\(1\le n\le100\)
\(1\le w_i,v_i\le100\)
\(1\le W\le10000\)
记忆化搜索
暴力搜索
递归方程:令 \(dfs(i,j)=\) 从编号为 \(i\) 的物品开始挑选出总重量不超过 \(j\) 的物品,所有挑选方案中价值总和的最大值。(编号从 0 开始,最后一个物品编号为 \(n-1\))
- 核心思想:对于编号为 \(i\) 的物品,可以选择
不拿
或者拿
不拿
:相当于从下一个物品(编号为 \(i+1\))开始挑选总重量仍然不超过 \(j\) 的物品,即 \(dfs(i+1,j)\)。
拿
:由于拿了重量为 \(w_i\) 的物品,则总重量由 \(j\) 变成了 \(j-w_i\)。相当于从下一个物品(编号为 \(i+1\))开始挑选总重量不超过 \(j-w_i\) 的物品,即 \(dfs(i+1,j-w_i)\)。 - 方程:
\[\begin{split}
dfs(n,j)&=0 \\
dfs(i,j)&=\begin{cases}
dfs(i+1,j)&,j<w_i \\
max\{dfs(i+1,j),dfs(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] -- 物品价值
// dfs(i, j) -- 从编号为i开始挑选总重不超过j的物品,返回最大价值
int dfs(int i, int j)
{
if (i == n)
return 0;
int ret;
if (j < w[i])
ret = dfs(i + 1, j);
else
ret = max(dfs(i + 1, j), dfs(i + 1, j - w[i]) + v[i]);
return ret;
}
// 输出
void solve(void)
{
printf("%d\n", dfs(0, W));
}
记忆化
我们用一组数据来看一下暴力搜索的过程。
n = 4, W = 5
(w, v) = {(2, 3), (1, 2), (3, 4), (2, 2)}
我们用二叉树的形式来描述该过程:
我们发现 \(dfs(3, 2)\) 执行了两次,这显然造成了浪费。如果能把第一次的结果记录下来,那么就避免了第二次的计算。这就是记忆化搜索,把搜索过程中的结果记录下来,避免重复的搜索。
// 输入
int n, W; // n -- 物品个数;W -- 最大重量
int w[WMAX], v[VMAX]; // w[WMAX] -- 物品重量;v[VMAX] -- 物品价值
int dp[MAX][MAX]; // 记忆化数组,必须足够大
// dfs(i, j) -- 从编号为i开始挑选总重不超过j的物品,返回最大价值
int dfs(int i, int j)
{
if (i == n)
return 0;
// 使用已经计算过的结果
if (dp[i][j] >= 0)
return dp[i][j];
int ret;
if (j < w[i])
ret = dfs(i + 1, j);
else
ret = max(dfs(i + 1, j), dfs(i + 1, j - w[i]) + v[i]);
return ret;
}
// 输出
void solve(void)
{
memset(dp, -1, sizeof(dp)); // 初始化记忆数组
printf("%d\n", dfs(0, W));
}
不难看出,与暴力搜索相比,记忆化搜索在原来的基础上,只不过多了 记忆化数组的声明和初始化 和 访问记忆化数组。
值得注意的是,记忆化数组必须开得足够大。因为它是用来记录 \(dfs(i,j)\) 的结果的,所以必须使 \(dp[i][j]\) 总是合法,不会越界访问。
动态规划
对于动态规划来说,最重要的就是 递推关系式。一般,我们可以先写出搜索算法,再得到递推式;我们也可以直接得出递推式。
由搜索递归函数得到递归式
由上文 暴力搜索 提到的函数递归式,我们可以写出动态规划的递推式。
\[\begin{split}
dp[n][j]&=0 \\
dp[i][j]&=\begin{cases}
dp[i+1][j]&,j<w_i \\
max\{dp[i+1][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 = n - 1; i >= 0; i--)
{
for (int j = 0; j <= W; j++)
{
if (j < w[i])
dp[i][j] = dp[i + 1][j];
else
dp[i][j] = max(dp[i + 1][j], dp[i + 1][j - w[i]] + v[i]);
}
}
printf("%d\n", dp[0][W]);
}
直接写出递归式
定义 \(dp[i][j]=\) 从前 \(i\) 个中选出总重量不超过 \(j\) 的物品。(因为编号从 \(0\) 开始,所以前 \(i+1\)个物品,最后一个物品的编号为 \(i\) )
\[\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]);
}
完
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具