【笔记】背包 dp

正在看著名的背包九讲,做点笔记 。

重要的状态转移方程

应该是最原始的那个 。

\[F[i][v] = max(F[i - 1][v], F[i - 1][v - c] + w) \]

说是很多背包的状态转移方程就是由这个变式来的……

01背包和完全背包

01背包

代码

for (int i = 1; i <= n; ++i)
  for (int v = vmax; v >= c[i]; --v)
    F[v] = max(F[v], F[v - c[i]] + w[i]);

一个小常数优化

将第二维枚举的下限改成 \(max(c[i], vmax - sum{c[i + 1 … n]})\)

可以用前缀和快速求出 。

原理:

  • 明确目标:求出 \(F[n][v]\)

  • 可以假设,当 \(F[i][v]\) 已经求了出来,用此更新 \(F[n][v]\)

  • 可以看出,中途 \(v\) 的最小值就是之后所有的物品都被装进背包里 。

  • 因此,用于更新 \(F[n][v]\) 的状态的 \(v\) 的最小值就是当前物品之后所有的物品都被装进背包里的时候 。

  • \(vmax - sum{c[i + 1 … n]}\)

完全背包

代码

for (int i = 1; i <= n; ++i)
  for (int v = c[i]; v <= vmax; ++v)
    F[v] = max(F[v], F[v - c[i]] + w[i]);

一些小优化

  • 对于 \(c[i] < c[j]\) 并且 \(w[i] > w[j]\)\(j\) 物品,可以直接扔掉 。

  • 可以把 \(c[i] > vmax\)\(i\) 物品扔掉 。

  • 可以在若干个 \(c[i]\) 相等的物品中找到 \(w[i]\) 最大的 。

一个二进制优化

  • 每种物品最多选取 \(\lfloor \frac{vmax}{c[i]} \rfloor\) 个 。

  • 将每种物品拆分成 \(2^0、2^1、2^2 \cdots 2^{k - 1}、\lfloor \frac{vmax}{c[i]} \rfloor - 2^k + 1\) 个,其中 \(\lfloor \frac{vmax}{c[i]} \rfloor - 2^k + 1 \geq 0\)\(k\) 是能取到的最大的整数 。

  • 因此可以将每种物品看做 \(2^0 \times c[i], ~2^0 \times w[i] \cdots (\lfloor \frac{vmax}{c[i]} \rfloor - 2^k + 1) \times c[i], ~(\lfloor \frac{vmax}{c[i]} \rfloor - 2^k + 1) \times w[i]\),保证 \((\lfloor \frac{vmax}{c[i]} \rfloor - 2^k + 1) \times c[i] \leq vmax\)

  • 由于每个数字都能由二进制表示出,因此拆分出的多组同种物品可以组出小于等于 \(\lfloor \frac{vmax}{c[i]} \rfloor\) 的所有数字 。

  • 每种物品的复杂度此时降到了 \(\Theta(log⌊\frac{V}{C_i}⌋)\)

代码区别

之前一直搞不懂为什么 \(v\) 的枚举顺序变了一下就不一样了,只是死记硬背下来,现在搞懂了。

  • \(F[v] = max(F[v], F[v - c[i]] + w[i])\) 是最原始式子的优化,把一维优化掉了 。

  • 因此在01背包中,要保证 \(F[v]\) 存的是 \(F[i - 1][v]\),之后再进行状态转移 。

  • 由于 \(v\) 是逆序枚举,因此当 \(v\) 改变了后,被刷新状态的 \(F[v]\) 中的 \(v\) 与当前的 \(v\) 比较更大 。

  • 因而当前的 \(F[v]\) 用来刷新的 \(F[v - c[i]]\) 还是上一个被枚举的 \(i\) 刷新的,保证了 \(F[v]\) 存的是 \(F[i - 1][v]\) 而不是 \(F[i][v]\)

完全背包同理 。

初始化 dp 数组

要求恰好装满背包

memset(F, -1, sizeof F);
F[0] = 0;

背包可以不被装满

memset(F, 0, sizeof F);

解释

可以从背包空间为 \(0\),物品数量为 \(0\) 的角度考虑 。

  • 第一种情况中 \(-1\) 代表未定义,因此此时只有 \(f[0][0] = 0\),保证了恰好装满背包

  • 第二种情况可以看作所有状态都为空,也就是可以为 \(0\),代表可以不被装满

多重背包

与01背包的区别:每种物品有规定的数量

代码

for (int i = 1; i <= n; ++i)
  for (int k = 1; k <= m[i] && (k * c[i]) <= vmax; ++k)
    for (int v = vmax; v >= k * c[i]; --v)
      F[v] = max(F[v], F[v - k * c[i]] + k * w[i]);

二进制优化

代码:

for (int i = 1; i <= n; ++i) {
  if (m[i] * c[i] >= vmax) {
    for (int v = c[i]; v <= vmax; ++v)
      F[v] = max(F[v], F[v - c[i]] + w[i]);
    continue;
  }
  int num = min(m[i], vmax / c[i]);
  for (int k = 1; num > 0; k <<= 1) {
    if (num < k) k = num;
    num -= k;
    for (int v = vmax; v >= k * c[i]; --v)
      F[v] = max(F[v], F[v - k * c[i]] + k * w[i]);
  }
}

当然也可以这样:

void Pre() {
  for (int i = 1; i <= n; ++i) {
    if (m[i] == 1) {
      C[++cnt] = c[i];
      W[cnt] = w[i];
      continue;
    }
    int num = min(m[i], vmax / c[i]);
    if (m[i] == 0) num = vmax / c[i];
    for (int k = 1; num > 0; k <<= 1) {
      if (k > num) k = num;
      num -= k;
      C[++cnt] = k * c[i];
      W[cnt] = k * w[i];
    }
  }
}
for (int i = 1; i <= cnt; ++i)
  for (int v = vmax; v >= C[i]; --v)
    F[v] = max(F[v], F[v - C[i]] + W[i]);

可行性判断

题目:每种有若干件的物品能否填满给定容量的背包 。

\(F[i][j]\) 表示用了前 \(i\) 种物品填满容量为 \(j\) 的背包后,最多还剩下几个第 \(i\) 种物品可用 。

代码:

memset(F, -1, sizeof F);
F[0][0] = 0;
for (int i = 1; i <= n; ++i) {
  for (int v = 0; v <= vmax; ++v) {
    if (F[i - 1][v] >= 0) F[i][v] = m[i];
    else F[i][v] = -1;
  }
  for (int v = 0; v <= vmax - c[i]; ++v)
    if (F[i][v] > 0)
      F[i][v + c[i]] = max(F[i][v + c[i]], F[i][v] - 1);
}

混合三种背包

最外层枚举不变,剩下的加个判断硬往上套就行了~~

二维费用背包

代码

for (int i = 1; i <= n; ++i)
  for (int v1 = vmax1; v1 >= c1[i]; --v1)
    for (int v2 = vmax2; v2 >= c2[i]; --v2)
      F[v1][v2] = max(F[v1][v2], F[v1 - c1[i]][v2 - c2[i]] + w[i]);

隐藏在题意下的二维费用背包:

每件物品最多取 \(U\) 件:可以将件数看做是一维费用,每取一件费用为 \(1\)

分组背包

代码

for (int k = 1; k <= knum; ++k)
  for (int v = vmax; v >= 0; --v)
    for (int i = 1; i <= n[k]; ++i)
      if (v >= c[i]) F[v] = max(F[v], F[v - c[i]] + w[i]);

\(k\) 是指枚举的第 \(k\) 组物品,\(v\) 枚举的是体积,\(i\) 枚举的是第 \(k\) 组物品中的第 \(i\) 件物品 。

枚举的顺序不能变

其中由于 \(v\) 是先于 \(i\) 枚举的,因此当 \(v\) 因枚举减小后,\(F[v]\) 并不会被 \(i\) 更新,即不会被同组的物品更新 。

相反,如果第 2,3 层枚举交换,

由于 \(i\) 是先于 \(v\) 枚举的,因此当 \(i\) 因枚举增大后,此时所有的 \(F[v]\) 已经被第 \(k\) 组的第 \(i - 1\) 个物品更新过了 。

一些小优化

  • 去掉每组物品中费用大价值小的物品

  • 排序或取最小值优化 \(v\) 的下限 。

多重背包是分组背包的一种特殊情况,所以可以用优化多重背包的方法来优化分组背包 。

可以这样看:

多重背包是从同一种物品中选 \(0, ~1, ~2 \cdots k\) 个,分组背包是从同一组物品中选第 \(0, ~1, ~2 \cdots k\) 个 。

多重背包的二进制优化是预处理出 \(k\) 种物品,所以我们可以预处理出分组背包每一组的 \(k\) 件物品 。

然而我并不知道怎么处理,不过这并不重要

说不定我哪天会了就放上来了呢

有依赖的背包

定义:\(i\) 依赖于 \(j\) 的意思是,选择了 \(i\) 物品就必须选择 \(j\) 物品 。

代码

for (int i = 1, k; i <= m; ++i) {
  c[i] = read(); w[i] = read(); k = read();
  if (k == 0) zhu[++knum] = i;
  else S[k].push_back(i);
} // 读入第 i 件物品的费用、价值、所依赖的主件,将主件存下,把附件存到主件下
for (int k = 1; k <= knum; ++k) { // 枚举主件所在的组
  n = S[zhu[k]].size();
  for (int j = 0, i; j < n; ++j) {
    i = S[zhu[k]][j]; // 枚举该主件下的附件
    for (int v = vmax; v >= c[i]; --v) // 用01背包得出第 k 件主件下附件的各最大价值
      f[k][v] = max(f[k][v], f[k][v - c[i]] + w[i]);
  }
}
for (int k = 1; k <= knum; ++k) // 枚举主件
  for (int v = vmax; v >= 0; --v) // 枚举费用
    for (int cv = v - c[zhu[k]]; cv >= 0; --cv) // 枚举该主件下的物品,费用为 cv,价值为 f[k][cv]
       ans[v] = max(ans[v], ans[v - cv - c[zhu[k]]] + f[k][cv] + w[zhu[k]]); // 分组背包,把主件的费用和价值算上

这是所有物品分为主件和附件的背包,一个附件只能有一个主件,而一个主件能有若干个附件 。

先对主件 \(k\) 的附件集合进行一次01背包,得到费用依次为 \(0、1、2 \cdots vmax - c[k]\) 的物品,数量为 \(vmax - c[k] + 1\)

每件物品与主件组成 \(vmax - c[k] + 1\) 种方案,把每个方案看做成一个物品,共同在第 \(k\) 组物品组里 。

\(k\) 组物品组里的每个物品的费用依次为 \(0、c[k]、c[k] + 1、c[k] + 2 \cdots vmax\),其中 \(0\) 是不选第 \(k\) 组物品组里的物品 。

价值依次为 \(0、F[k][0] + w[k]、F[k][1] + w[k]、F[k][2] + w[k] \cdots F[k][vmax - c[k]] + w[k]\)

其他的一些拓展

有森林关系的有依赖的背包,其本质是树形dp,就要看下面的泛化物品了 。

泛化物品

这时候就需要度娘了呢~~

概念

一种物品,它并没有固定的费用和价值,而它的价值随着你分配给它的费用而变化,可以看做是一个函数 。

所有的物品都可以被看做是泛化物品

  • 01背包中的物品:仅有 \(h(c) = w\),剩下的都为 \(0\)

  • 完全背包中的物品:仅有 \(h(c) = w, ~h(2c) = 2w, ~h(3c) = 3w \cdots h(kc) = kw ~(k \leq \frac{vmax}{c} ~且~ k \in Z^+)\),剩下的都为 \(0\)

  • 多重背包中的物品:仅有 \(h(c) = w, ~h(2c) = 2w, ~h(3c) = 3w \cdots h(kc) = kw ~(k \leq min(m, ~\frac{vmax}{c}) ~且~ k \in Z^+)\),剩下的都为 \(0\)

  • 二维费用背包中的物品:仅有 \(h(c1, ~c2) = w\),剩下的都为 \(0\)

  • 分组背包中的物品:把一个物品组看作成一个泛化物品,若物品组中存在费用为 \(c\) 的物品,则 \(h(c)\) 取值为所有费用为 \(c\) 的物品的最大价值,剩下的都为 \(0\)

  • 有依赖的背包中的物品:每个主件及其附件集合等价于一个物品组,可看作一个泛化物品 。

泛化物品的和的公式

\[F(v) = max(h(k) + l(v − k)), ~(0 \leq k \leq v) \]

泛化物品的背包问题

对于其中的物品都是泛化物品的背包问题,求它的答案的过程也就是求所有这些泛化物品之和的过程

若问题的和为 \(F\) 函数,则答案就是 \(F(0 \cdots vmax)\) 中的最大值 。

持续更新

posted @ 2021-10-05 20:17  _Scaley  阅读(92)  评论(0编辑  收藏  举报