从背包问题说起——初学者角度看背包问题

从背包问题说起

本文聚焦DP、记忆化搜索、滚动数组在0-1背包问题中的运用,并简要探讨多重背包问题中拆分思想的实践。

背包问题分为几大类,0-1背包问题、完全背包问题、多重背包问题、混合背包问题。0-1背包问题中每个对象只能选取一次,其他问题都是在0-1背包基础上进行推演的。

0-1背包

以0-1背包问题的表述为例:有n个物品和容量为\(W\)的背包,每个物品分别具有\(wi\)和价值\(vi\)两个属性,求W给定的情况下背包能够装入的物品的最大价值

我们首先考虑一个能够产生子问题的选择方式,这个选择方式需要能够获得一个最优解。通俗一点,即选择一个解决问题A的方法,其需要先解决子问题(的集合)B,而在确定B能够获得最优解的情况下保证A也能获得最优解。保持子问题尽可能简单而避免扩展(即尽可能不要1生2、2生4,而应当是1生1)。

在背包问题中,我们假设从第一个物品按顺序选到第n个物品,而最终解决的问题就是在决定是否选择第n个物品时\(W\)容量所能承载的最大价值的问题(设为Final Problem,FP)。而为了解决FP,我们做出选择,从而将FP分解为子问题:

  • 决定是否选择第n-1个物品时\(W\)容量所能承载的最大价值\(F_{n-1,W}\)

  • 决定是否选择第n-1个物品时\(W-w_n\)所能承载的最大价值\(F_{n-1,W-w_n}\)

\[F_{n,W} = max(F_{n-1,W},F_{n-1,W-w_n} + v_n) \]

我们采用记忆化搜索的思想,将\(dp[i][j]\)作为第\(i\)个物品时容量最大为W的最大价值,每次需要重新计算时,先查一次记忆化表,从而避免子问题的二次计算。

在这里我们考虑利用滚动数组优化。滚动数组是动态规划问题中常用的优化方式,主要用于优化空间复杂度。假设我们开一个二维数组,分别对应第\(i\)个物品和当前容量\(j\),那么实际上我们会发现,\(F_{n,W}\)实际上只需要\(F_{n-1,W}\)\(F_{n-1,W-w_n}\)就能得出,也就是说,\(F_{n-2到0}\)都在最终计算时用不上,那么我们就可以将这个二维数组压缩到一维,每次都复用数组空间来实现压缩空间的目的,这就是滚动数组的基本思想:通过反复运用同一块空间来实现数组的“滚动”。

因此优化后我们可以得到:

\[F_W = max(F_W,F_{W-w_i}+v_n) \]

以上就是状态转移方程的推导

注意到这个方程是几乎所有背包问题的基础

这里需要注意一点,等式左侧的\(F_W\)实际上对应选取第n个物品时容量为W的最大价值,而右侧的\(F_W\)对应的是取第n-1个物品时容量为W的最大价值,由于滚动数组,这两个变量共享了同一块地址空间,因此说明了在物品选择的维度上我们需要从0到n递增

下面我们来看一个代码,从而理解物品容量的维度上我们应该如何处理

for(int i=0;i<n;i++){
    for(int j=0;j<=W-w[i];j++){
        f[j+w[i]] = max(f[j]+v[i],f[j+w[i]]);
    }
}

这个代码在W的求解顺序上有问题,为什么呢?

大家考虑在固定\(i=x\)的情况下,说明我们正在研究第x个物品,那么我们发现,在求取\(W\)的维度时,我们先求取了f[0]、f[1]……,这导致了什么问题呢?我们发现\(f[w]\)需要基于\(f[w-w[i]]\)而得出,逻辑上代表了\(f[w-w[i]]\)的最大价值加上第x个物品的价值,而\(f[w-w[i]]\)是在\(f[w]\)前求取的,那么\(f[w-w[i]]\)实际上是\(f[w-w[i]-w[i]]\)的最大价值加上第x个物品的价值,你会发现第x个物品在求取f[w]时已经被放进去多次!,这与题目的0-1性质相悖。

实际上,多重背包问题的解法就是这样的顺序

正确的顺序应该是:

for(int i=0;i<n;i++){
    for(int j=W;j>=w[i];j--){
        f[j] = max(f[j-w[i]]+v[i],f[j);
    }
}

完全背包

我们更近一步,考虑一个物品可能可以选择无限次,得到完全背包问题。

在获取第\(i\)个物品时容量为\(j\)对应的最大价值时,要考虑上一个物品时容量为\(j\)时最大价值和考虑第i个物品时容量为\(j-w_i\)时的最大价值。

\[F_{i,j} = max(F_{i-1,j},F_{i,j-w_i}+v_i) \]

再次考虑滚动数组优化,可见\(F_i\)\(F_{i-1}\)得出,而与其之前的无关,因此可以把物品个数的维度去掉,使其优化为一维的空间。

\[F_{j} = max(F_{j},F_{j-w_i}+v_i) \]

注意这里和0-1问题不同,在考虑第\(i\)个物品时,需要从容量为0时逐个考虑将第\(i\)个物品装入的价值,因此求取顺序上与0-1不同。此处按下不表、

多重背包

多重背包问题中,每个物品不再能无限次选取,而只能最多选取\(k_i\)次。我们当然可以将其看为\(k_i\)个相同物品,然后和0-1背包问题一样看待,这样的复杂度为\(O(nWmax(k_i))\)

然后考虑优化问题,我们可以注意到,对三个完全一致的物品ABC,同时选AB和同时选BC是一样的,但是我们将其视为两种不同的子问题进行了求解,因此如何解决这种重复求解问题是优化的关键。

如何将\(k_{i}\)个相同的物品进行拆分,使得不需要对每个物品单独考虑子问题空间,是一个有效的优化想法。而考虑对不定数量物品拆分,可以考虑二进制分组优化。二进制分组优化的核心在于,任意一个整数n,都能拆分成2的整数幂的和与一个余数的集合,而对任意i<=n,i都能表达为这个集合子集的和

因此我们将\(k_i\)拆分为1+2+4+8+……和一个余数,使得每1、2、4、8……个物品整合成为一个“大物品”,然后对这些大物品组合成的物品列表考虑0-1背包问题即可。

index = 0;
for (int i = 1; i <= m; i++) {
  int c = 1, p, h, k; // k个重量为p、价值为h的物品
  cin >> p >> h >> k;
  while (k - c > 0) {
    k -= c;
    list[++index].w = c * p;
    list[index].v = c * h;
    c *= 2;
  }
  list[++index].w = p * k;
  list[index].v = h * k;
} // 二进制分组的逻辑代码

混合背包

前面三种背包问题的缝合怪,此处按下不表。

二维费用背包

当考虑的物品不仅具有重量这一个限制条件,还含有价格这一新的限制条件,求取容量和经费均有限的情况下的最大价值。

实际上就是多了层循环,注意体会这个转换(以下以0-1背包问题展示):

for(int i=0;i<=n;i++){
	for(int money=max_money;money >= money_i;money++){
		for(int cap=max_cap;cap>=weight_i;cap++){
			f[money][cap]=max(f[money][cap],
             f[money-money_i][cap-weight_i]+v[i]);
		}
	}
}
posted @ 2021-03-24 09:54  neumy  阅读(113)  评论(0编辑  收藏  举报