背包问题常见bug

ops/2025/2/19 9:41:27/

关于背包问题的常见问题的讨论

不同背包问题的遍历顺序问题

01背包问题和完全背包问题的遍历顺序相反,主要是因为它们在状态转移过程中对物品的使用方式不同。以下是详细的分析:

1. 01背包问题

  • 问题描述:每个物品最多只能使用一次。

  • 状态定义:dp[i][j] 表示在前 i 个物品中,选择若干个物品放入容量为 j 的背包中,能够获得的最大价值。

  • 状态转移方程

    • 如果不选择第 i 个物品:dp[i][j] = dp[i - 1][j]
    • 如果选择第 i 个物品:dp[i][j] = dp[i - 1][j - w[i]] + v[i](前提是 j >= w[i])
  • 遍历顺序

    • 外层循环:遍历物品(从第1个到第 n 个)。
    • 内层循环:遍历背包容量(从大到小)。这是因为如果从大到小遍历,每次更新 dp[j] 时,dp[j - w[i]] 仍然是上一轮的状态(即不包含当前物品的状态),从而避免重复计算。

2. 完全背包问题

  • 问题描述:每个物品可以使用任意多次。

  • 状态定义:dp[i][j] 表示在前 i 个物品中,选择若干个物品放入容量为 j 的背包中,能够获得的最大价值。

  • 状态转移方程

    • 如果不选择第 i 个物品:dp[i][j] = dp[i - 1][j]
    • 如果选择第 i 个物品:dp[i][j] = dp[i][j - w[i]] + v[i](前提是 j >= w[i])
  • 遍历顺序

    • 外层循环:遍历物品(从第1个到第 n 个)。
    • 内层循环:遍历背包容量(从小到大)。这是因为如果从小到大遍历,每次更新 dp[j] 时,dp[j - w[i]] 已经包含了当前物品的状态,从而可以多次使用当前物品。

3. 遍历顺序不同的原因

  • 01背包问题

    • 每个物品只能使用一次,为了避免重复计算,需要确保在更新状态时,当前物品的状态只被使用一次。因此,从大到小遍历背包容量,确保每次更新 dp[j] 时,dp[j - w[i]] 是未包含当前物品的状态。
  • 完全背包问题

    • 每个物品可以使用多次,需要在更新状态时,允许当前物品多次被使用。因此,从小到大遍历背包容量,确保每次更新 dp[j] 时,dp[j - w[i]] 已经包含了当前物品的状态,从而可以多次累加。

4. 总结

  • 01背包问题:从大到小遍历背包容量,避免重复计算。
  • 完全背包问题:从小到大遍历背包容量,允许多次使用当前物品。

这种遍历顺序的差异是基于问题的约束条件和状态转移方程的设计,确保算法能够正确地处理物品的使用限制。

例题

  1. 混合背包问题

有 N 种物品和一个容量是 V 的背包。

物品一共有三类:

  • 第一类物品只能用1次(01背包);
  • 第二类物品可以用无限次(完全背包);
  • 第三类物品最多只能用 si 次(多重背包);

每种体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

输入格式

第一行两个整数,N,V用空格隔开,分别表示物品种数和背包容积。

接下来有 NN 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

  • si=−1 表示第 i 种物品只能用1次;
  • si=0 表示第 i 种物品可以用无限次;
  • si>0 表示第 i种物品可以使用si 次;
输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤10000<N,V≤1000
0<vi,wi≤10000<vi,wi≤1000
−1≤si≤1000−1≤si≤1000

输入样例
4 5
1 2 -1
2 4 1
3 4 0
4 5 2
输出样例:
8

解析:在这个问题中我们就要清楚完全背包,01背包的遍历顺序的区别

#include<iostream>
#include<cstring> 
#include<algorithm> 
using namespace std; const int N = 1010; // 定义一个常量N,表示背包容量和物品数量的上限
int dp[N];          // 定义一个动态规划数组,用于存储每个容量下的最大价值
int v, w, s, n, m;  // 定义变量:v表示物品体积,w表示物品价值,s表示物品数量,n表示物品总数,m表示背包容量int main() {cin >> n >> m; // 输入物品总数n和背包容量mfor (int i = 0; i < n; i++) { // 遍历每个物品cin >> v >> w >> s; // 输入当前物品的体积v、价值w和数量sif (!s) { // 如果s为0,表示这是一个完全背包问题for (int j = v; j <= m; j++) { // 从体积v开始,遍历所有可能的背包容量dp[j] = max(dp[j], dp[j - v] + w); // 更新dp[j],表示在容量j下,可以放入当前物品的最大价值}}if (s == -1) s = 1; // 如果s为-1,将其转换为1,表示这是一个01背包问题for (int k = 1; k <= s; k *= 2) { // 使用二进制拆分法,将多重背包问题转化为多个01背包问题for (int j = m; j >= v * k; j--) { // 从容量m开始,逆序遍历所有可能的背包容量dp[j] = max(dp[j], dp[j - k * v] + k * w); // 更新dp[j],表示在容量j下,可以放入k个当前物品的最大价值}s -= k; // 减去已经处理过的物品数量}if (s) { // 如果还有剩余的物品数量for (int j = m; j >= s * v; j--) { // 从容量m开始,逆序遍历所有可能的背包容量dp[j] = max(dp[j], dp[j - s * v] + s * w); // 更新dp[j],表示在容量j下,可以放入剩余数量的当前物品的最大价值}}}cout << dp[m] << endl; // 输出最终结果,即背包容量为m时的最大价值return 0;
}

状态更新的完整性问题

在这里我们讨论的主要是关于边界dp[0][0] = 0的更新问题;

例题

  1. 潜水员

潜水员为了潜水要使用特殊的装备。

他有一个带2种气体的气缸:一个为氧气,一个为氮气。

让潜水员下潜的深度需要各种数量的氧和氮。

潜水员有一定数量的气缸。

每个气缸都有重量和气体容量。

潜水员为了完成他的工作需要特定数量的氧和氮。

他完成工作所需气缸的总重的最低限度的是多少?

例如:潜水员有5个气缸。每行三个数字为:氧,氮的(升)量和气缸的重量:

3 36 12010 25 1295 50 2501 45 1304 20 119

如果潜水员需要5升的氧和60升的氮则总重最小为249(1,2或者4,5号气缸)。

你的任务就是计算潜水员为了完成他的工作需要的气缸的重量的最低值。

输入格式

第一行有2个整数 m,n。它们表示氧,氮各自需要的量。

第二行为整数 kk 表示气缸的个数。

此后的 kk 行,每行包括ai,bi,ci3个整数。这些各自是:第 i 个气缸里的氧和氮的容量及气缸重量。

输出格式

仅一行包含一个整数,为潜水员完成工作所需的气缸的重量总和的最低值。

数据范围

1≤m≤21
1≤n≤79
1≤k≤1000
1≤ai≤21
1≤bi≤79
1≤ci≤800

输入样例:
5 60
5
3 36 120
10 25 129
5 50 250
1 45 130
4 20 119
输出样例:
249

错误代码:

#include<iostream>
#include<cstring>
using namespace std;
int dp[22][80];
int m,n,k,a,b,c;
int main(){cin>>m>>n>>k;memset(dp,0x3f,sizeof(dp));dp[0][0]=0;for(int i=0;i<k;i++){cin>>a>>b>>c;for(int j=m;j>=a;j--){for(int l=n;l>=b;l--){dp[j][l]=min(dp[j][l],dp[j-a][l-b]+c);}}}cout<<dp[m][n]<<endl;return 0;
}
输入
5 60
5
3 36 120
10 25 129
5 50 250
1 45 130
4 20 119输出
1061109567

正确代码:

#include<iostream>
#include<cstring>
using namespace std;
int dp[22][80];
int m,n,k,a,b,c;
int main(){cin>>m>>n>>k;memset(dp,0x3f,sizeof(dp));dp[0][0]=0;for(int i=0;i<k;i++){cin>>a>>b>>c;for (int j = m; j >= 0; j--) {for (int l = n; l >= 0; l--) {dp[j][l] = min(dp[j][l], dp[max(0, j - a)][max(0, l - b)] + c);}}}cout<<dp[m][n]<<endl;return 0;
}
输入
5 60
5
3 36 120
10 25 129
5 50 250
1 45 130
4 20 119输出
249

他们的主要区别就是当我们把将循环条件改写为:

for (int j = m; j >= a; j--) {for (int l = n; l >= b; l--) {dp[j][l] = min(dp[j][l], dp[j - a][l - b] + c);}
}

虽然在某些情况下可以工作,但这种写法存在潜在问题,可能会导致错误的结果。以下是具体原因:

1. 状态更新的完整性问题

动态规划中,我们需要确保所有可能的状态都被正确更新。原代码:

for (int j = m; j >= 0; j--) {for (int l = n; l >= 0; l--) {dp[j][l] = min(dp[j][l], dp[max(0, j - a)][max(0, l - b)] + c);}
}

这种方式确保了以下几点:

  • 边界条件的处理max(0, j - a)max(0, l - b) 确保即使 j - al - b 小于 0,也不会出现数组越界问题。这使得状态转移更加安全。
  • 所有状态的更新:即使某些状态(如 j < al < b)无法通过当前分配方式直接到达,这些状态仍然会被考虑(通过其他方式或初始状态)。

而改写后的代码:

for (int j = m; j >= a; j--) {for (int l = n; l >= b; l--) {dp[j][l] = min(dp[j][l], dp[j - a][l - b] + c);}
}
  • 循环范围的限制j >= al >= b 的条件限制了循环范围,这意味着当 j < al < b 时,这些状态不会被更新。这可能导致某些状态被遗漏。
  • 边界状态的忽略:如果 ab 很大,可能会直接跳过一些边界状态(如 j = 0l = 0),导致这些状态的值没有被正确更新。

2. 状态转移的正确性问题

动态规划中,状态转移的正确性依赖于所有相关状态都被正确处理。原代码通过 max(0, j - a)max(0, l - b) 确保了即使某些状态无法直接到达,也会被正确初始化(如 dp[0][0] = 0),从而保证了状态转移的正确性。

而改写后的代码直接跳过了 j < al < b 的状态,可能会导致以下问题:

  • 状态依赖的破坏:某些状态可能依赖于边界状态(如 dp[0][0]),如果这些边界状态没有被正确更新,可能会导致错误的结果。
  • 状态更新的不完整性:某些状态可能无法通过当前分配方式到达,但仍然需要通过其他方式更新。改写后的代码可能会遗漏这些状态。

3. 具体例子

假设:

  • m = 5, n = 5(资源总量)
  • a = 3, b = 3, c = 1(分配方式)

原代码:

for (int j = 5; j >= 0; j--) {for (int l = 5; l >= 0; l--) {dp[j][l] = min(dp[j][l], dp[max(0, j - 3)][max(0, l - 3)] + 1);}
}
  • 这会正确更新所有状态,包括边界状态(如 dp[2][2] 会从 dp[0][0] 更新)。

改写后的代码:

for (int j = 5; j >= 3; j--) {for (int l = 5; l >= 3; l--) {dp[j][l] = min(dp[j][l], dp[j - 3][l - 3] + 1);}
}
  • 这会跳过 j < 3l < 3 的状态,导致 dp[2][2] 没有被更新,最终结果可能会不正确。

4. 总结

改写后的代码虽然在某些情况下可以工作,但存在以下问题:

  • 边界状态的忽略:可能导致某些状态没有被正确更新。
  • 状态更新的不完整性:可能会遗漏一些状态,导致最终结果不正确。

因此,原代码通过 max(0, j - a)max(0, l - b) 确保了所有状态都被正确处理,是更安全和更完整的实现方式。

  • 源代码适用于状态转移可能会涉及到边界值的情况,且边界值是合法的,并且需要处理数组越界问题。
  • 修改后的代码适用于状态转移不会涉及到边界值的情况,且边界值是非法的,或者不需要参与状态转移。它更简洁,但需要确保状态转移过程中不会出现越界问题。

http://www.ppmy.cn/ops/157029.html

相关文章

日志统计(acWing,蓝桥杯)

题目&#xff1a; 1238. 日志统计 题目 提交记录 讨论 题解 视频讲解 小明维护着一个程序员论坛。现在他收集了一份”点赞”日志&#xff0c;日志共有 NN 行。 其中每一行的格式是&#xff1a; ts id 表示在 tsts 时刻编号 idid 的帖子收到一个”赞”。 现在小明想…

支持多种网络数据库格式的自动化转换工具——VisualXML

一、VisualXML软件介绍 对于DBC、ARXML……文件的编辑、修改等繁琐操作&#xff0c;WINDHILL风丘科技开发的总线设计工具——VisualXML&#xff0c;可轻松解决这一问题&#xff0c;提升工作效率。 VisualXML是一个强大且基于Excel表格生成多种网络数据库文件的转换工具&#…

e2studio开发RA2E1(8)----GPT定时器频率与占空比的设置

e2studio开发RA2E1.8--GPT定时器频率与占空比的设置 概述视频教学样品申请硬件准备参考程序源码下载选择计时器时钟源PWM(脉冲宽度调制)R_GPT_PeriodSet()函数说明R_GPT_DutyCycleSet()函数说明R_GPT_Reset()函数说明R_GPT_Close() 函数说明主程序波形情况 概述 GPT&#xff0…

CUDA Graph

cudaGraphLaunch 是 NVIDIA CUDA API 中的一个函数&#xff0c;用于在 CUDA Graphs 中启动一个已实例化的图。 CUDA Graphs 简介 CUDA Graphs 是 NVIDIA CUDA 编程模型中的一种技术&#xff0c;旨在优化 GPU 程序的性能。它允许将一系列连续的 GPU 操作&#xff08;如计算和数…

C# OpenCV机器视觉:老照片修复

阿强是个念旧的人&#xff0c;家里珍藏着满满一箱子老照片。这些照片承载着他童年的欢笑、家人的温暖&#xff0c;还有那些一去不复返的旧时光。然而&#xff0c;岁月这把无情的 “杀猪刀”&#xff0c;不仅在阿强的脸上留下了痕迹&#xff0c;也让这些老照片受尽了 “折磨”。…

直接插入排序

一&#xff1a;直接插入排序是什么。 二&#xff1a;如何实现直接插入排序 三&#xff1a;直接插入排序时间复杂度 一&#xff1a;直接插入排序它是排序得一种&#xff0c;其目的无非是将乱序通过排序排成有序的。 我们可以通过动画直观看什么是直接插入排序 这是我找的直接…

人工智能入门 数学基础 线性代数 笔记

必备的数学知识是理解人工智能不可或缺的要素&#xff0c;今天的种种人工智能技术归根到底都建立在数学模型之上&#xff0c;而这些数学模型又都离不开线性代数&#xff08;linear algebra&#xff09;的理论框架。 线性代数的核心意义&#xff1a;世间万事万物都可以被抽象成某…

人工智能D* Lite 算法-动态障碍物处理、多步预测和启发式函数优化

在智能驾驶领域&#xff0c;D* Lite 算法是一种高效的动态路径规划算法&#xff0c;适用于处理环境变化时的路径重规划问题。以下将为你展示 D* Lite 算法的高级用法&#xff0c;包含动态障碍物处理、多步预测和启发式函数优化等方面的代码实现。 代码实现 import heapq impo…