背包问题学习笔记Part1

背包问题

前言:背包问题的学习笔记和模板例题整理。

01背包

\(N\)件物品和一个容量为\(V\)的背包。第\(i\)件物品的费用是\(c_i\),价值是\(w_i\)。求解将哪些物品装入背包可使价值总和最大。

分析:

  • 每种物品都只有一件。
  • 都只有放或是不放两种选择。
  • \(f_{i,v}\)表示只放入前\(i\)个物品时在所用体积为\(v\)时所能得到的最大价值。
  • 由此可以得出状态转移方程式:\(f_{i,v}=\max(f_{i-1,v},f_{i-1,v-c_i}+w_i)\)

解释:

  • 这是一个背包问题的原始状态转移方程式,有很多背包问题的变体都是从此推出而得
  • 将前\(i\)个物品放入背包这一过程只会与前\(i-1\)个物品有关,也就是相当于只考虑\(i\)件物品是否放入的问题
  • 如果不放入第\(i\)件物品,那么问题转化为\(i-1\)件物品放入容量为\(v\)的背包中,表示出来就是此时:\(f_{i,v}=f_{i-1,v}\)
  • 如果放入第\(i\)件物品,那么问题转化为\(i-1\)件物品放入容量为\(v-c_i\)的背包中,表示出来就是此时:\(f_{i,v}=f_{i-1,v-c_i}+w_i\)
  • 因为要取其中的最优策略,所以要取两种情况中的较大值

空间复杂度优化:

  • 01背包的空间复杂度其实可以优化到\(O(N)\)级别
  • 上面讲的思路如果要实现,首先要有一层\(1 \dots n\)的循环,控制每一次循环得出某个\(i\)时的所有\(f_{i,v}\)的值
  • 是否能只用一个一维数组\(f_{0 \dots v}\)就保证在第\(i\)次循环推\(f_{i,v}\)的值时得到的\(f_v\)就是之前的\(f_{i-1,v}\)\(f_{i-1,v-c_i}+w_i\)的值呢?
  • 其实在循环时用\(v \dots 1\)的顺序推出\(f_v\),这样就能保证得到的\(f_v\)就是\(f_{i-1,v}\)\(f_{i-1,v-c_i}+w_i\)中的较大值

核心部分代码:

for(int i=1;i<=n;i++)
    for(int j=m;j>=1;j--)
        f[j]=max(f[j-c[i]]+w[i],f[j]);

一些细节:

  • 其实可以对上面的代码进行一个常数优化,因为如果循环中的\(j\)比当前的\(c_i\)要小,便没有继续推的必要,可以把for(int j=m;j>=1;j--)改为for(int j=m;j>=c[i];j--)
  • 如果要求的是恰好装满背包情况下的最大价值,那么除了\(f_0\)赋值为\(0\),除此之外的\(f_{1 \dots m}\)都要赋为极小值\(-\infty\)
  • 如果要求的是不必装满背包情况下的最大价值(比如下面那道模板题),那么整个\(f_{0 \dots m}\)都赋值为\(0\)
  • 分成这两种情况的原因是:初始化数组\(f\)时应该给其赋值在没有任何物品放入时的合法状态
  • 如果要求恰好装满,那么没有物品情况下的唯一合法情况就是容量为\(0\)的情况被“一无所有”装满时的价值\(0\),其他容量的背包均无合法的解,处于未定义状态,所以赋值为\(-\infty\)
  • 如果不要求恰好装满,那么此时每个容量下的背包都有合法情况即不装任何物品,所以\(f_{0 \dots m}\)都赋值为\(0\)
  • 关于初始化的问题可以推广到其他种类的背包问题

模板题:

P1048 采药
code

完全背包

\(N\)种物品和一个容量为\(V\)的背包,每种物品都有无限件可用。第\(i\)种物品的费用是\(c_i\),价值是\(w_i\)。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

分析:

  • 这个问题和01背包问题唯一的差别是这个问题里的物品可以取无数件
  • 所以取物品的策略从取和不取两种决策变成取\(0 \dots \frac{m}{c_i}\)
  • 此时如果按照01背包的思想,令\(f{i,v}\)为前\(i\)个物品放入体积为\(v\)的背包中的最大价值,依然可以推出这种思路下的状态转移方程式:
  • \(f_{i,v}=\max(f_{i-1,v-k*c_i}+k*w_i \mid 0 \le c_i*k \le v)\)
  • 这样做和01背包一样,有\(O(NV)\)种情况,但是每种状况的讨论时间复杂度不在是常数。求出状态\(f_{i,v}\)所需的时间复杂度是\(O{(\frac{V}{c_i}})\),总时间复杂度变为\(O({V* \sum_\frac{V}{c_i}})\)

优化:

  • 完全背包问题有一个很简单有效的优化,是这样的:若两件物品\(i\)\(j\)满足\(c_i=w_j\),则将物品\(j\)去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高得\(j\)换成物美价廉的\(i\),得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。
  • 这个优化可以简单的\(O(N^2)\)地实现,一般都可以承受。另外,针对背包问题而言,比较不错的一种方法是:首先将费用大于\(V\)的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以\(O(V+N)\)地完成这个优化。

转化:

  • 考虑把完全背包问题转化为01背包问题来解。最简单的想法是,考虑到第i种物品最多选\(\frac{V}{c_i}\)件,于是可以把第\(i\)种物品转化为\(\frac{V}{c_i}\)件费用及价值均不变的物品,然后求解这个01背包问题。这样完全没有改进基本思路的时间复杂度,但这毕竟给了我们将完全背包问题转化为01背包问题的思路:将一种物品拆成多件物品。
  • 更高效的转化方法是:把第\(i\)种物品拆成费用为\(c_i*2^k\)、价值为\(w_i*2^k\)的若干件物品,其中\(k\)满足\(c_i*2^k \le V\)。这是二进制的思想,因为不管最优策略选几件第\(i\)种物品,总可以表示成若干个\(2^k\)件物品的和。这样把每种物品拆成\(log \frac{V}{c_i}\)件物品,是一个很大的改进。

\(O(VN)\)的算法:

先放上代码:

for(int i=1;i<=n;i++)
    for(int j=c[i];j<=m;j++)
        f[j]=max(f[j],f[j-c[i]]+w[i]);

可以看出,上面的代码用的是一维数组,与01背包相比,只有第二重循环的循环次序有所不同。
为什么这样改就行?

  • 01背包按照\(m \dots 0\)的顺序循环,是为了保证当前状态\(f_{i,v}\)是由\(f_{i-1,v-c_i}\)推得,即保证每件物品只选择一次
  • 而完全背包的特点就是物品有无数件,所以在决定状态\(f_{i,v}\)时,要考虑到状态\(f_{i,v-c_i}\)
  • 或者将基本思路中的方程式带入原方程,可以得到:\(f_{i,v}=\max(f_{i,v},f_{i,v-c_i}+w_i)\)

模板题:

P1616 疯狂的采药
code

多重背包

\(N\) 种物品和一个容量为 \(V\) 的背包。第 \(i\) 种物品最多有 \(n_i\) 件可用,每件费用是 \(c_i\),价值是 \(w_i\)。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

解法

其实多重背包与完全背包很相似,基本的状态转移方程也只是与之略有不同。
对于第 \(i\) 件物品,有 \(n_i+1\) 种策略:取 \(0 \dots n_i\) 件。
还是令 \(f_{i,v}\) 为考虑前 \(i\) 个物品恰好放入 \(v\) 的空间时的最大价值。
此时的状态转移方程就是: $$f_{i,v}= \max(f_{i-1,v-k \times c_i}+k \times w_i \mid 0 \le k \le n_i)$$
时间复杂度是: \(O(V \times \sum n_i)\)
多重背包也能二进制优化。

\(O(VN)\) 的解法

单调队列优化DP,本人不会,咕着,以后再补

甚至没有完全的模板题,不过多重背包应该还算好理解,毕竟有前面的铺垫了。

后记

背包问题学习笔记Part1就只写前三种最为经典的背包问题吧。
后面几种不是特别经典的留到后面。
在此之前要写另一篇博客,记录前三种背包问题的几道不是很难的变式题目,毕竟只会个模板完全不够。

完结撒花

posted @ 2022-01-23 20:32  AIskeleton  阅读(38)  评论(0编辑  收藏  举报