zoukankan      html  css  js  c++  java
  • 【笔记】背包 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]} floor) 个 。

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

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

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

    • 每种物品的复杂度此时降到了 (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)) 中的最大值 。

    持续更新

  • 相关阅读:
    iOS中使用nil NULL NSNULL的区别
    Xcode常用快捷键总结
    (求租仓库)navigationController .navigationBar 的属性设置
    imageNamed 与 initWithContentsOfFile 区别
    iOS-Senior10-多线程(子线程创建)
    iOS-setValue和setObject的区别
    iOS-Senior8-网络之进阶
    iOS-Senior7-数据请求
    iOS-Senior6-数据解析(JSON)
    iOS-Senior6-数据解析(XML)
  • 原文地址:https://www.cnblogs.com/EiffelA-blog/p/15369431.html
Copyright © 2011-2022 走看看