--- title: "02-周氏背包九讲" created: 2025-11-28 tags: - 博客 aliases: - 周氏背包九讲 --- # 周氏背包九讲 在讲dp问题之前 先了解一下什么是dp 它的核心是什么 我想最简单的方法应该是从递归和递推入手 逐渐引入到dp问题 再然后就慢慢学着直接用dp的思路(闫氏dp分析法)想问题 首先第一步 ## 从递归递推到dp 以两道例题的形式来引入 ### 题目1:跳台阶 一个楼梯共有 n 级台阶,每次可以走一级或者两级,问从第 0 级台阶走到第 n 级台阶一共有多少种方案。 输入格式 共一行,包含一个整数 n。 输出格式 共一行,包含一个整数,表示方案数。 数据范围 1≤n≤15 #### 分析: ##### 递归(dfs) 对于任意一个台阶级数 都可以分为由它-1级走一步到达 由它-2级走两步到达 比如 七级台阶可以分成6级台阶走一步 5级台阶走两步 然后再递归处理6级和5级的情况 由此生成这样一棵递归搜索树 ![[image-e8f38227.png]] 最小状态 2级台阶有两种走法(0走两次1步 和 0走一次两步) 1级台阶只有一种走法(0走一步) 递归的解法就这样出来了 ```cpp #include using namespace std; int n; int dfs(int x) { //1 2是不可再分的 所以直接返回 if(x==1) return 1; else if(x==2) return 2; //对于其他情况就可以一直拆解递归 else return dfs(x-1)+dfs(x-2); } int main() { cin>>n; int res=dfs(n); cout< using namespace std; const int N=20; int mem[N];//新增记忆化数组 int n; int dfs(int x) { //若已存过 就返回记录的结果 if(mem[x]) return mem[x]; int sum=0; if(x==1) sum = 1; else if(x==2) sum = 2; else sum=dfs(x-1)+dfs(x-2); mem[x]=sum; return sum; } int main() { cin>>n; int res=dfs(n); cout< using namespace std; const int N=20; int dp[N]; int n; int main() { cin>>n; dp[1]=1,dp[2]=2; //把dfs的归部分 状态转移 成这样的递推公式 for(int i=3;i<=n;i++) dp[i]=dp[i-1]+dp[i-2]; cout< using namespace std; int n; int main() { cin>>n; int a,b,fn; a=1,b=2; for(int i=1;i<=n;i++) { if(i==n) cout< using namespace std; const int N=100010; int home[N]; int n,T; int dfs(int x) { if(x>n) return 0; //不同处在于这里返回的是max而不是和 else //如果该点不偷就往左走找临近的店 如果偷就往隔两个的店考虑 并加上当前累计值 return max(dfs(x+1),dfs(x+2)+home[x]); } int main() { cin>>T; while(T--) { cin>>n; for(int i=1;i<=n;i++) cin>>home[i]; int res=dfs(1);//从第一个店看 偷不偷 cout< using namespace std; const int N=100010; int home[N]; int mem[N];//添加记忆化数组 int n,T; int dfs(int x) { if (mem[x]) return mem[x]; int sum=0; if(x>n) sum=0; else sum= max(dfs(x+1),dfs(x+2)+home[x]); mem[x]=sum; return sum; } int main() { cin>>T; while(T--) { cin>>n; for(int i=1;i<=n;i++) cin>>home[i]; memset(mem,0,sizeof mem);//记得这里重置记忆化数组 int res=dfs(1); cout< using namespace std; const int N=100010; int home[N]; int f[N]; int n,T; int main() { cin>>T; while(T--) { cin>>n; for(int i=1;i<=n;i++) cin>>home[i]; memset(f,0,sizeof f); //因为1是由2 3的状态(小的根据大的)得出的 所以n应该由大到小 for(int i=n;i>=1;i--) { f[i]=max(f[i+1],f[i+2]+home[i]); } cout< using namespace std; const int N=1010; int v[N],w[N]; int n,m; int dfs(int x,int spV) { if(x>n) return 0; //容量不够放下该物品时 只能跳过该物品继续往后走 else if(spV=v[x]) //如果放了 容积就会减小 且背包内价值会增大 return max(dfs(x+1,spV),dfs(x+1,spV-v[x])+w[x]); } int main() { cin>>n>>m; for(int i=1;i<=n;i++) cin>>v[i]>>w[i]; int res=dfs(1,m); cout< using namespace std; const int N=1010; int v[N],w[N]; int n,m; int mem[N][N];//记忆化数组(两个参数所以二维) int dfs(int x,int spV) { if(mem[x][spV]) return mem[x][spV]; int sum=0; if(x>n) sum = 0; else if(spV=v[x]) sum = max(dfs(x+1,spV),dfs(x+1,spV-v[x])+w[x]); mem[x][spV]=sum; return sum; } int main() { cin>>n>>m; for(int i=1;i<=n;i++) cin>>v[i]>>w[i]; int res=dfs(1,m); cout< using namespace std; const int N=1010; int v[N],w[N]; int n,m; int f[N][N]; int main() { cin>>n>>m; for(int i=1;i<=n;i++) cin>>v[i]>>w[i]; //递归搜索树里树叶是较大 树根是较小 所以n~1往前推 for(int i=n;i>=1;i--) { //遍历体积 for(int j=0;j<=m;j++) { if(j=v[i])//如果放得下 就要取选或不选 结果更大的那种 { f[i][j]=max(f[i+1][j],f[i+1][j-v[i]]+w[i]); } } } cout< using namespace std; const int N=1010; int v[N],w[N]; int n,m; int f[N][N]; int main() { cin>>n>>m; for(int i=1;i<=n;i++) cin>>v[i]>>w[i]; //第0个物品无需考虑 f[0][0~m]的最大价值永远是0 初始化成0(全局) //从第1个物品开始考虑 for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ //还是一样的 左边的集合是一定存在的(不选) //右边的集合有可能是空集 (剩余体积不够时) f[i][j]=f[i-1][j]; if(j>=v[i]) f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]); } } cout< using namespace std; const int N=1010; int v[N],w[N]; int n,m; int f[N]; int main() { cin>>n>>m; for(int i=1;i<=n;i++) cin>>v[i]>>w[i]; for(int i=1;i<=n;i++) for(int j=m;j>=v[i];j--) //因为 j-v[i] using namespace std; const int N = 1010; int v[N], w[N]; int f[N][N]; int n, m; int main() { cin >> n >> m; for(int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i]; for(int i = 1; i <= n; i ++ ) for(int j = 0; j <= m; j ++ ) //其实就是在01的基础上 加上一个遍历每个物品放多少次的循环 for(int k = 0; k * v[i] <= j; k ++ )//可以无限取 但不能超过剩余容量 f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]); //f[i-1][j]包含在后半部分 即k=0的时候 cout << f[n][m] << endl; } ``` 显而易见是tle的 #### 分析 用闫氏dp分析法来看 ![[image-5f4dbcff.png]] 从三个步骤进行考虑。 **步骤一:集合和集合的状态** 所谓的集合,就是一些方案的集合。 用 g[i][j] 表示从前 i 种物品中进行选择,且总体积不大于 j 的各个选法获得的价值的集合。 注意:g[i][j] 不是一个数,是一堆数。 例如 g[2][3] 从前 2 种物品中进行选择,且总体积不大于 3 的各个选法获得的价值的集合。 g[2][3] 的可选择方案包括: 方案一:都不选,总价值为 0。 方案二:选 1 件 物品 1,总价值为 2。 方案三:选 2 件物品 1,总价值为 4。 方案四:选 3件 物品 1,总价值为 6。 方案五:选 1 件物品 2,总价值为 4。 方案六:选 1 件物品 2,一件物品 1,总价值为 6。 所以 g[2][3] = {0,2,4,6,4,2}。 i j 取不同的值,对应不同的 g[i][j],也就是对应不同的集合。 用 f[i][j] 表示从前 i 种物品中进行选择,总体积小于等于 j 所能获得的**最大价值**。很明显,f[i][j] 就是 g[i][j] 中的最大值。i j 取不同的值,就对应不同的 f[i][j]。我们把 f[i][j] 叫做集合的状态。 例如 f[2][3] 表示从前 2 种物品中进行选择,且总体积不大于 3 的获得的最大价值。 f[2][3] = max(g[2][3] ) = max( 0,2,4,6,4,2) = 6。 g[i][j] 的最大值就是 f[i][j]。 如果我们能把所有集合对应的最大值都求出来,即求出了 f[0][0] ~ f[N][V], f[N][V] 的含义是在前 N 种物品中进行选择,总体积不大于 V 所获得的最大价值,就是我们要找的答案。 ![[image-55069e9d.png]] 注意,我们不需要把各个集合的所有元素都找出来,只需要求出各个集合的最大值,就能找到答案。下面就是如何求出各个集合的最大值。 **步骤二:状态计算** g[i][j] 是从前 i 种物品中进行选择,且总体积不大于 j 的各个选法获得的价值的集合。 f[i][j] 是从前 i 种物品中进行选择,总体积小于等于 j 所能获得的最大价值。 f[i][j] 是集合 g[i][j] 的最大值。 ![[image-18f076ff.png]] 所谓的状态计算是指,如何将把 f[i][j] 算出来。 如果把各个集合 g[i][j] 的状态 f[i][j] 求出来, f[N][V] 就是要找的答案。 回想一下 0 1 背包问题。 01 背包问题把 g[i][j]划分成了 A B 两部分,分别求出这两个部分对应的最大值,然后两者取最大值就是整体 g[i][j] 的最大值,就是 f[i][j]。 01 背包根据是否选择第 i 件物品,也就是第 i 件物品选 0 个还是 1 个,把 g[i][j] 划分成了 A B 两部分,分别求出这两个部分的最大值,然后两者取最大值就是整体 g[i][j] 的最大值,也就求出了 f[i][j]。 完全背包问题也是根据第 i 件物品的选择数量,把 g[i][j] 划分成不同的部分,分别求出各个部分的最大值,取各个部分最大值中的最大值,就是整体 g[i][j] 的最大值,也就求出了 f[i][j]。 因为每种物品的数量是无限的,根据第 i 种物品的选择数量可以把 g[i][j] 分为这样几部分: A 部分: 第 i 种物品选 0 件。 B 部分:第 i 件物品选 1 件。 C 部分: 第 i 件物品选 2 件。 X 部分: 第 i 件物品选 x 件。 ![[image-50c7871e.png]] 因为选择物品的总体积不能大于j,所以第 i 件物品最多选 j / vi 向下取整 件。 对于 A 部分:第 i 件物品选 0 件。 等价于从前 i - 1 种物品中选择商品,且总体积不超过 j 的各个价值的集合,也就是 g[i - 1][j]。 g[i - 1][j] 这个集合中的最大值是 f[i - 1][j] ,所以 A 部分的最大值就是 f[i - 1][j]。 对于 B 部分:第 i 件物品选 1 件, 1 个 i 物品会占据 vi的背包空间,剩下的背包空间为 j - vi 。 可以从前 i - 1 种物品中,选出总体积小于等于j - vi 的物品放入背包。 从前 i - 1 种物品中,选出总体积小于等于j - vi 的各个方案获得的价值集合为 g[i - 1][j - vi ], 所以 B 部分的元素为 g[i - 1][j - vi ] 中各个元素加上 wi 。 g[i - 1][j - vi ] 中的最大值为 f[i - 1][j - vi ],所以 B 部分的最大值为 f[i - 1][j - vi ] + wi。 对于 X 部分:第 i 件物品选 x 件, x 个 i 物品会占据 x \* vi 的背包空间,剩下的背包空间为 j - x \* vi 。 可以从前 i - 1 种物品中,选出总体积小于等于j - x \* vi 的物品放入背包。 从前 i - 1 种物品中,选出总体积小于等于j - x \* vivi 的各个方案获得的价值集合为 g[i - 1][j - x \* vi ], 所以 x 部分的元素为 g[i - 1][j - x \* vi ] 中各个元素加上 x \* wi 。 g[i - 1][j - x \* vivi ] 中的最大值为 f[i - 1][j - x \* vi ],所以 B 部分的最大值为 f[i - 1][j - x \* vi ] + x \* wi。 例如 g[2][4]。 第二种物品的体积为 2,选择物品的总体积不能超过 4。 所以第二件物品可以选择:0件、1件、2件。 因此 g[2][4] 可以分成以下几部分: A 部分:第二件物品选 0 件。A 部分的最大值为: f[i - 1][j - 0 \* vi] + 0 \* wi 。 B 部分:第二件物品选 1 件。B部分的最大值为: f[i - 1][j - 1 \* vi ] + 1 \* wi 。 C 部分:第二件物品选 2 件。C 部分的最大值为:f[i - 1][j - 2 \* vi ] + 2 \* wi 。 g[2][4] 中的最大值为 max(A,B,C)。 通过上面分析,我们可以知道,g[i][j] 可以分成若干部分: A 部分是第 i 种物品选 0 个对应所有选法获的价值的集合,最大值是 f[i - 1][j]。 B 部分是第 i 种物品选 1 个对应所有选法获的价值的集合,最大值是 f[i-1][j - vi] + wi。 X 部分是第 i 种物品选 x 个对应所有选法获的价值的集合,最大值是 f[i - 1][j - x \* vi]+x\*wi。 所以 g[i][j] 的最大值就是所有子集的最大值中最大的那个,也就是 f[i][j] = max(A, B ,····) 即: 展开式为: f[i] [j] = max( f[i-1][j] , f[i - 1][j - vi]+w , f[i - 1][j - 2 \* vi] + 2 \* w , f[i - 1][j - k \* vi ] + k \* w , …..) 其中 k <= j / w。 从计算公式可以看出: f[i][j] 是由 f[i - 1][j - k \* vi ] (0 <= k <= j / wi) 和 wi 计算出来的。 f[i][j]的值是可以从前面已经计算出的 f 值求出来。 如果我们能确定 f[i][j] 的一部分初始值,就能通过该公式,一步步计算得出 f[N][V],也就是我们要找的答案。 **步骤三:确定初始值** 完全背包问题的有些状态是能够直接确定的。 例如 f[0][0]。 f[0][0] 的含义是: 从前 0 种物品中选择,并且选出的物品总体积小于等于0 时所能得到的最大价值。 总体积小于等于 0,说明一种物品都不能选择。 因此 f[0][0] = 0。同理 f[1][0] = 0,f[2][0] = 0 ··· f[N][0] = 0。 有了这些初始值,通过 i 从 1 遍历 N,j 从 1 遍历 V,第 i 种物品的选择数量 k 从 0 遍历到 j / wi 就能一步步求出所有的 f[i][j] 了。 例如 求 f[1][1]: f[1][1] = max{f[0][1],f[0][0] + 2} = max(0,2) = 2。 求 f[1][2]: f[1][2] = max{f[0][2],f[0][1] + 2,f[0][0] + 4} = max(0,2,4) = 4。 最后 f[N][V] 就是要找的答案。 #### 优化 ![[image-123ec702.png]] ```cpp #include using namespace std; const int N = 1010; int v[N], w[N]; int f[N][N]; int n, m; int main() { cin >> n >> m; for(int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i]; for(int i = 1; i <= n; i ++ ) for(int j = 0; j <= m; j ++ ) /* 三重循环会tle 试着把这层循环去掉 for(int k = 0; k * v[i] <= j; k ++ ) f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]); 把k=1 2 3……代入 f[i,j] = Max(f[i-1,j] , f[i-1,j-v]+w , f[i-1,j-2v]+2w , f[i-1,j-3v]+3w...) f[i,j-v] = Max( f[i-1,j-v] , f[i-1,j-2v]+w , f[i-1,j-3v]+2w...) 可以发现从 f[i,j-v] -> f[i,j] 有很多项相似 也就是说 加入第i个物品 状态变化有规律可言 (本来就是要依靠子问题去更新当前问题的答案 所以提取出上一层与当前层的关系 状态转移方程就好写了) 规律就是 f[i,j]其实就是除第一项外 其他项为f[i,j-v]+w (每一项比原本多了一个w罢了) 那么状态转移方程就可以写成:f[i,j] =Max(f[i-1,j],f[i,j-v]+w) 这样一来就又变成了01背包问题类似的代码 所以得加上个判断 可放入的情况和不可放入的情况 */ { //不可放入的情况 直接用上一级答案 f[i][j] = f[i-1][j]; //可放入的情况 if(j>=v[i]) //将找到的规律变形 第一项没规律,保留 其他项有规律 为上一级+w f[i][j] =max(f[i-1][j], f[i][j - v[i]] + w[i]); //f[i][j] =max(f[i][j], f[i][j - v[i]] + w[i]);(前面有f[i][j] = f[i-1][j];) } cout << f[n][m] << endl; } ``` 那么可以发现 现在的核心代码和01背包问题的非常相似了 `f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);//01背包` `f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);//完全背包问题` 唯一的区别在于 01背包是从上一层i-1的状态得来 而完全背包是从这一层的i的状态得来 那么同样也可以用滚动数组优化 这次要的是滚动后覆盖后的值 所以可以从小到大枚举 ```cpp #include using namespace std; const int N = 1010; int v[N],w[N]; int f[N]; int n, m; int main() { cin >> n >> m; for(int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i]; for(int i = 1; i <= n; i ++ ) for(int j = v[i]; j <= m; j ++ )//要的是第i层的覆盖后的j-v[i] 所以从小到大枚举 f[j] = max(f[j], f[j-v[i]] + w[i]); cout << f[m] << endl; } ``` ### 对比01与完全 **所以到最后 发现和01背包问题只有一个地方不一样——体积是从小到大遍历还是从大到小遍历** **而究其原因 就在于 要的是第i-1层的未被覆盖的数据 还是第i层的覆盖后的数据** **从大到小是未覆盖的值 从小到大是覆盖后的值** ### 多重背包问题 有 N 种物品和一个容量是 V 的背包。 第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。 输出最大价值。 输入格式 第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。 接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。 输出格式 输出一个整数,表示最大价值。 #### 朴素写法 仅适用于数据范围100时 在完全背包的基础上 进行一个物品个数的限制 即每个物品并不是无限个 而是有个数限制的 所以只需要在第三轮的k循环中 加上一个k≤s[i]的限制即可 这种朴素做法 在数据范围小的时候有效 数据范围一大 就会tle了 ```cpp #include using namespace std; const int N=110; int v[N],w[N],s[N]; int dp[N][N]; int n,m; int main() { cin>>n>>m; for(int i=1;i<=n;i++) cin>>v[i]>>w[i]>>s[i]; for(int i=1;i<=n;i++){ for(int j=0;j<=m;j++){ for(int k=0;k<=s[i] && k*v[i]<=j;k++){//只需要加一个k<=s[i]的限制 dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]*k]+k*w[i]); } } } cout< using namespace std; const int N = 12010, M = 2010; int n, m; int v[N], w[N]; //逐一枚举最大是N*logS int f[M]; // 体积> n >> m; int cnt = 0; //分组的组别 for(int i = 1;i <= n;i ++) { int a,b,s; cin >> a >> b >> s; int k = 1; // 组别里面的个数 while(k<=s) { cnt ++ ; //组别先增加 v[cnt] = a * k ; //整体体积 w[cnt] = b * k; // 整体价值 s -= k; // s要减小 k *= 2; // 组别里的个数增加 } //剩余的一组 if(s>0) { cnt ++ ; v[cnt] = a*s; w[cnt] = b*s; } } n = cnt ; //枚举次数正式由个数变成组别数 //01背包一维优化 for(int i = 1;i <= n ;i ++) for(int j = m ;j >= v[i];j --) f[j] = max(f[j],f[j-v[i]] + w[i]); cout << f[m] << endl; return 0; } ``` **问题1:为什么最后一项会是f[i−1,j−(S+1)v]+Sw** 在完全背包中,通过两个状态转移方程: f[i,j] = max( f[i−1,j], f[i−1,j−v]+w, f[i−1,j−2v]+2w, f[i−1,j−3v]+3w,…..) f[i,j−v] = max( f[i−1,j−v], f[i−1,j−2v]+w, f[i−1,j−3v]+2w,…..) 通过上述比较,可以得到 f[i][j]=max(f[i−1][j],f[i][j−v]+w) 再来看下多重背包, f[i,j] = max( f[i−1,j], f[i−1,j−v]+w, f[i−1,j−2v]+2w, ….. f[i−1,j−Sv]+Sw,) f[i,j−v] = max( f[i−1,j−v], f[i−1,j−2v]+w, ….. f[i−1,j−Sv]+(S−1)w, f[i−1,j−(S+1)v]+Sw) 怎么比完全背包方程比较就多出了一项? 其实,一般从实际含义出发来考虑即可,这里是在分析f[i,j−v] 这个状态的表达式,首先这个状态的含义是 从前i个物品中选,且总体积不超过j-w的最大价值, 我们现在最多只能选s个物品,因此如果我们选s个第i个物品,那么体积上就要减去 s∗v,价值上就要加上s∗w,那更新到状态中去就是 f[i−1,j−v−s∗v]+s∗w 那为什么完全背包不会有最后一项? 完全背包由于对每种物品没有选择个数的限制,所以只要体积够用就可以一直选,没有最后一项。 **问题2:为什么不能和完全背包一样优化** 正如上分析 多重背包比完全背包多了一项 而最大值这个操作是不能做什么同时减去一个数 最大值仍不变的 所以不可以简单地从f[i,j-v]直接加上一个w转移到f[i,j] **问题3:二进制优化 为什么正确** 首先确认三点: (1)我们知道转化成01背包的基本思路就是:判断每件物品是取了还是不取 (2)我们知道任意一个实数可以由二进制数来表示,也就是$2^0$ $2^k$其中一项或几项的和。 (3)这里多重背包问的就是每件物品取多少件可以获得最大价值。 分析: 如果直接遍历转化为01背包问题,是每次都拿一个来问,取了好还是不取好。 那么根据数据范围,这样的时间复杂度是$O(n^3)$,也就是 $10^9$,这样是毫无疑问是会TLE的。 假如10个取7个好,那么在实际的遍历过程中在第7个以后经过状态转移方程其实已经是选择“不取”好了。 现在,用二进制思想将其分堆,分成k+1个分别有2k个的堆,然后拿这一堆一堆去问,是取,还是不取,经过dp选择之后,结果和拿一个一个来问的结果是完全一样的,因为dp选择的是最优结果,而根据第二点任意一个实数都可以用二进制来表示,如果最终选出来10个取7个是最优的在分堆的选择过程中分成了2^0=1,2^1=2,2^2=4,10−7=3这四堆,然后去问四次,也就是拿去走dp状态转移方程,走的结果是第一堆1个,取了比不取好,第二堆2个,取了比不取好,第三堆四个,取了比不取好,第四堆8个,取了还不如不取,最后依旧是取了1+2+4=7个 如果仍然不是很能理解的话,取这样一个例子:要求在一堆苹果选出n个苹果。 我们传统的思维是一个一个地去选,选够n个苹果就停止。这样选择的次数就是n次 二进制优化思维就是:现在给出一堆苹果和10个箱子,选出n个苹果。 将这一堆苹果分别按照1,2,4,8,16,…..512分到10个箱子里, 那么由于任何一个数字x∈[0,1023](第11个箱子才能取到1024) 都可以从这10个箱子里的苹果数量表示出来,但是这样选择的次数就是 ≤10次 比如: - 如果要拿1001次苹果,传统就是要拿1001次;二进制的思维,就是拿7个箱子就行(分别是装有512、256、128、64、32、8、1个苹果的这7个箱子),这样一来,1001次操作就变成7次操作就行了。 这样利用二进制优化,时间复杂度就从 $O(n^3)$降到 $O(n^2logS)$, 从$4∗10^9$降到了 $2∗10^7$ 视频: ##### 单调队列优化 **多重背包的原始状态转移方程** f(i,j)=max(f(i−1,j),f(i−1,j−v)+w,⋯,f(i−1,j−sv)+sw) **考虑用完全背包的优化方式来优化这个方程** f(i,j−v)=max(f(i−1,j−v),f(i−1,j−2v)+w,⋯,f(i−1,j−(s+1)v)+(s)w) 写出这个公式好像并不是那么管用 因为 完全背包 是一口气把所有体积全部用掉,即 max(a,b,c,d)=max(a,max(b,c,d)) 然而 多重背包 对于每个物品的个数是有限制的,导致我们最终的等式是如下样子: max(a,b,c,d)≠max(a,max(b,c,d,e)) 但是,我们可以把这个式子 继续 推导下去,直到背包**体积被用到不能再用**为止 ![[image-ecc03f84.png]] 其中 r=j mod vi,也可以理解为 完全背包 下把当前物品 选到不能再选 后,剩下的 余数 得到 f(i,r)=f(i−1,r)后,我们再利用 完全背包优化思路 往回倒推一遍 会惊奇的发现一个 滑动窗口求最大值 的模型,具体如下: 为了方便观察,把 f(i−1,j)改写成 fj ![[image-982117dd.png]] 可能看上去还是有点复杂,为了更方便观察,去掉 w,然后把数组展开成一条链 具体如下图: ![[image-ead7af88.png]] 于是通过该 滑动窗口 ,我们就能在 线性 的时间里求出 i 阶段里,所有满足 j≡r mod (v)的 f(i,j) 滑动窗口 求 最大值 的实现,只需利用 队列 在队头维护一个 最大值 的 单调递减 的 单调队列 即可 为了更新所有 i 阶段里的状态 f(i,j),我们只需再额外枚举所有的 余数 r 即可 不要忘记,滑动窗口内部比较最大值的时候,有一个在之前为了方便观察,被删掉的偏移量 w 要记得加上再比较 具体就是 当前下标 和该 最大值的下标 之间差了 x个 v,那么就要加上 x个 w 在上面公式里,还是比较容易看出的吧,就不做额外的推导了 代码 二维朴素版 时间复杂度:O(n×v) 空间复杂度:O(n×v) 滑动窗口的长度为 si+1 ```cpp #include using namespace std; const int N = 1010, M = 20010; int n, m; int v[N], w[N], s[N]; int f[N][M]; int q[M]; int main() { cin >> n >> m; for (int i = 1; i <= n; ++ i) cin >> v[i] >> w[i] >> s[i]; for (int i = 1; i <= n; ++ i) { for (int r = 0; r < v[i]; ++ r) { int hh = 0, tt = -1; for (int j = r; j <= m; j += v[i]) { while (hh <= tt && j - q[hh] > s[i] * v[i]) hh ++ ; while (hh <= tt && f[i - 1][q[tt]] + (j - q[tt]) / v[i] * w[i] <= f[i - 1][j]) -- tt; q[ ++ tt] = j; f[i][j] = f[i - 1][q[hh]] + (j - q[hh]) / v[i] * w[i]; } } } cout << f[n][m] << endl; return 0; } ``` 一维优化 时间复杂度:O(n×v) 空间复杂度:O(v) 和 01背包 的优化类似,观察到 状态转移方程,对于 i 阶段,只会用到 i-1 层的状态 因此可以采用 拷贝数组 或 滚动数组 的写法 拷贝数组写法 ```cpp #include #include using namespace std; const int N = 1010, M = 20010; int n, m; int v[N], w[N], s[N]; int f[M], g[M]; int q[M]; int main() { cin >> n >> m; for (int i = 1; i <= n; ++ i) cin >> v[i] >> w[i] >> s[i]; for (int i = 1; i <= n; ++ i) { memcpy(g, f, sizeof g); for (int r = 0; r < v[i]; ++ r) { int hh = 0, tt = -1; for (int j = r; j <= m; j += v[i]) { while (hh <= tt && j - q[hh] > s[i] * v[i]) hh ++ ; while (hh <= tt && g[q[tt]] + (j - q[tt]) / v[i] * w[i] <= g[j]) -- tt; q[ ++ tt] = j; f[j] = g[q[hh]] + (j - q[hh]) / v[i] * w[i]; } } } cout << f[m] << endl; return 0; } ``` 滚动数组写法 ```cpp #include using namespace std; const int N = 1010, M = 20010; int n, m; int v[N], w[N], s[N]; int f[2][M]; int q[M]; int main() { cin >> n >> m; for (int i = 1; i <= n; ++ i) cin >> v[i] >> w[i] >> s[i]; for (int i = 1; i <= n; ++ i) { for (int r = 0; r < v[i]; ++ r) { int hh = 0, tt = -1; for (int j = r; j <= m; j += v[i]) { while (hh <= tt && j - q[hh] > s[i] * v[i]) hh ++ ; while (hh <= tt && f[(i - 1) & 1][q[tt]] + (j - q[tt]) / v[i] * w[i] <= f[(i - 1) & 1][j]) -- tt; q[ ++ tt] = j; f[i & 1][j] = f[(i - 1) & 1][q[hh]] + (j - q[hh]) / v[i] * w[i]; } } } cout << f[n & 1][m] << endl; return 0; } ``` ### 混合背包问题 有 N 种物品和一个容量是 V 的背包。 物品一共有三类: - 第一类物品只能用1次(01背包); - 第二类物品可以用无限次(完全背包); - 第三类物品最多只能用 si 次(多重背包); 每种体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。 输出最大价值。 输入格式 第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。 接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。 - si=−1 表示第 i 种物品只能用1次; - si=0 表示第 i 种物品可以用无限次; - si>0 表示第 i 种物品可以使用 si 次; 输出格式 输出一个整数,表示最大价值。 数据范围 0 using namespace std; const int N=1010; struct Thing{ int kind; int v,w; }; vector things; int dp[N]; int n,m; int main() { cin>>n>>m; for(int i=1;i<=n;i++){ int v,w,s; cin>>v>>w>>s; if(s==-1)//如果该物品是01背包的 存-1标记 things.push_back({-1,v,w}); else if(s==0) things.push_back({0,v,w}); else{//如果是多重背包的 把它拆成多个01背包 for(int k=1;k<=s;k*=2){ things.push_back({-1,v*k,w*k}); s-=k; } if(s>0) things.push_back({-1,v*s,w*s}); } } for(auto thing:things){ if(thing.kind==-1) for(int j=m;j>=thing.v;j--)//01背包 从大到小 dp[j]=max(dp[j],dp[j-thing.v]+thing.w); else for(int j=thing.v;j<=m;j++)//完全背包 从小到大 dp[j]=max(dp[j],dp[j-thing.v]+thing.w); } cout< using namespace std; const int N=1010; int v[N],w[N],s[N]; int dp[N]; int n,m; int main() { cin>>n>>m; for(int i=1;i<=n;i++){ cin>>v[i]>>w[i]>>s[i]; } //可以不用事先存好 直接现做 for(int i=1;i<=n;i++){ if(s[i]==0)//完全背包 从小到大 for(int j=v[i];j<=m;j++) dp[j]=max(dp[j],dp[j-v[i]]+w[i]); else//01和多重合在一起 { if(s[i]==-1) s[i]=1;//01背包就是该物品只能选一次的情况 直接让s[i]>0 且等于1即可 for(int k=1;k<=s[i];k*=2){ for(int j=m;j>=k*v[i];j--){ dp[j]=max(dp[j],dp[j-k*v[i]]+k*w[i]); } s[i]-=k; } if(s[i]){ for(int j=m;j>=s[i]*v[i];j--){ dp[j]=max(dp[j],dp[j-s[i]*v[i]]+s[i]*w[i]); } } } } cout< using namespace std; const int N=1010,M=110; int v[N],m[N],w[N];//每件物品的体积、重量和价值 int f[N][M][M]; int n,m1,m2;// 物品数量、背包容积上限、背包重量上限 int main() { cin>>n>>m1>>m2; for(int i=1;i<=n;i++) cin>>v[i]>>m[i]>>w[i]; for(int i=1;i<=n;i++) { for(int j1=0;j1<=m1;j1++){ for(int j2=0;j2<=m2;j2++){//加一层循环即可 状态转移时也要多一 f[i][j1][j2]=f[i-1][j1][j2]; if(j1>=v[i] && j2>=m[i]) f[i][j1][j2]=max(f[i-1][j1][j2],f[i-1][j1-v[i]][j2-m[i]]+w[i]); } } } cout< using namespace std; const int N=1010,M=110; int v[N],m[N],w[N]; int f[M][M]; int n,m1,m2; int main() { cin>>n>>m1>>m2; for(int i=1;i<=n;i++) cin>>v[i]>>m[i]>>w[i]; for(int i=1;i<=n;i++) { for(int j1=m1;j1>=v[i];j1--){ for(int j2=m2;j2>=m[i];j2--){ f[j1][j2]=max(f[j1][j2],f[j1-v[i]][j2-m[i]]+w[i]); } } } cout< using namespace std; const int N = 110; //由n种物品变成了n类物品 然后又要在每类里面去选 //所以就是多了一维 int v[N][N],w[N][N],s[N]; int f[N][N]; int n, m; int main() { cin>>n>>m; for(int i=1;i<=n;i++){ cin>>s[i]; for(int j=0;j>v[i][j]>>w[i][j]; } } for(int i=1;i<=n;i++){ for(int j=0;j<=m;j++){ f[i][j]=f[i-1][j]; //不选 for(int k=0;k=v[i][k]) f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]); } } } cout< using namespace std; const int N = 110; //由n种物品变成了n类物品 然后又要在每类里面去选 //所以就是多了一维 int v[N][N],w[N][N],s[N]; int f[N]; int n, m; int main() { cin>>n>>m; for(int i=1;i<=n;i++){ cin>>s[i]; for(int j=0;j>v[i][j]>>w[i][j]; } } for(int i=1;i<=n;i++){ //用的上一层的数据 从大到小遍历 for(int j=m;j>=0;j--){ for(int k=0;k=v[i][k]) f[j]=max(f[j],f[j-v[i][k]]+w[i][k]); } } } cout< using namespace std; const int N=110,M=10010; int v[N]; int f[N][M]; int n,m; int main() { cin>>n>>m; for(int i=1;i<=n;i++) cin>>v[i]; //从i个物品中选 且总价值等于0的方案数都是一个(什么都不选也是一种选法) for(int i=0;i=v[i]) f[i][j]+=f[i-1][j-v[i]]; } } cout< using namespace std; const int N=110,M=10010; int v[N]; int f[M]; int n,m; int main() { cin>>n>>m; for(int i=1;i<=n;i++) cin>>v[i]; f[0]=1; for(int i=1;i<=n;i++) for(int j=m;j>=v[i];j--) f[j]+=f[j-v[i]]; cout< using namespace std; const int M=10010; int f[M]; int n,m; int main() { cin>>n>>m; f[0]=1; for(int i=1;i<=n;i++){ int v;cin>>v; for(int j=m;j>=v;j--) f[j]+=f[j-v]; } cout< using namespace std; const int N = 1010, mod = 1e9 + 7; int v[N], w[N]; int f[N][N], g[N][N]; int n, m; int main() { cin >> n >> m; for (int i = 1; i <= n; i++) cin >> v[i] >> w[i]; // 初始化方案数为1,即不选任何物品的情况 for (int i = 0; i <= n; i++) g[i][0] = 1; for (int i = 1; i <= n; i++) { for (int j = 0; j <= m; j++) { f[i][j] = f[i-1][j]; // 不选第i个物品 g[i][j] = g[i-1][j]; // 继承不选的方案数 if (j >= v[i]) { if (f[i][j] < f[i-1][j-v[i]] + w[i]) { f[i][j] = f[i-1][j-v[i]] + w[i]; // 更新最大价值 g[i][j] = g[i-1][j-v[i]]; // 更新方案数 } else if (f[i][j] == f[i-1][j-v[i]] + w[i]) { g[i][j] = (g[i][j] + g[i-1][j-v[i]]) % mod; // 累加方案数 } } } } int res = 0; for (int j = 0; j <= m; j++) { if (f[n][j] == f[n][m]){ res = (res + g[n][j]) % mod; } } cout << res; return 0; } ``` 接着就是老套路 消掉一维 ```cpp #include using namespace std; const int N = 1010, mod = 1e9 + 7; int v[N], w[N]; int f[N], g[N]; int n, m; int main() { cin >> n >> m; for (int i = 1; i <= n; i++) cin >> v[i] >> w[i]; g[0] = 1; for (int i = 1; i <= n; i++) { for (int j = m; j >= v[i]; j--) { if (f[j] < f[j-v[i]] + w[i]) { f[j] = f[j-v[i]] + w[i]; // 更新最大价值 g[j] = g[j-v[i]]; // 更新方案数 } else if (f[j] == f[j-v[i]] + w[i]) { g[j] = (g[j] + g[j-v[i]]) % mod; // 累加方案数 } } } int res = 0; for (int j = 0; j <= m; j++) { if (f[j] == f[m]){ res = (res + g[j]) % mod; } } cout << res; return 0; } ``` #### 背包问题求具体方案 有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。 第 i 件物品的体积是 vi,价值是 wi。 求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出 **字典序最小的方案**。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 1…N。 输入格式 第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。 接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。输出格式 输出一行,包含若干个用空格隔开的整数,表示最优解中所选物品的编号序列,且该编号序列的字典序最小。 物品编号范围是 1…N。 数据范围 0 using namespace std; const int N=1010; int w[N],v[N]; int f[N][N]; int path[N],cnt; int n,m; int main() { cin>>n>>m; for(int i=1;i<=n;i++) cin>>v[i]>>w[i]; for(int i=n;i>=1;i--) { for(int j=0;j<=m;j++) { f[i][j]=f[i+1][j]; if(j>=v[i]) f[i][j]=max(f[i][j],f[i+1][j-v[i]]+w[i]); } } for(int i=1,j=m;i<=n;i++) { // 判断是否选取了当前物品 if(j>= v[i] && f[i][j]==f[i+1][j-v[i]]+w[i]){ path[cnt++]=i;// 记录选取的物品 j-=v[i];// 更新剩余容量 } } for(int i=0;i