Unverified Commit 62e31650 authored by Shuhao Zhang's avatar Shuhao Zhang Committed by GitHub
Browse files

Merge branch 'master' into master

parents a9b1de6a 9f9ccf66
Loading
Loading
Loading
Loading
+70 −20
Original line number Diff line number Diff line
在学习本章前请确认你已经学习了[动态规划部分简介](/dp/)

在具体讲何为 "背包 dp" 前,先来看如下的例题
在具体讲何为 背包 dp 前,先来看如下的例题

??? note " [\[USACO07 DEC\]Charm Bracelet](https://www.luogu.org/problemnew/show/P2871)"
??? note " [USACO07 DECCharm Bracelet](https://www.luogu.org/problemnew/show/P2871)"
    题意概要:有 $n$ 个物品和一个容量为 $W$ 的背包,每个物品有重量 $w_{i}$ 和价值 $v_{i}$ 两种属性,要求选若干物品放入背包使背包中物品的总价值最大且背包中物品的总重量不超过背包的容量。
    
在上述例题中,由于每个物体只有 $2$ 种可能的状态(取与不取),正如二进制中的 $0$ 和 $1$,这类问题便被称为 “0-1 背包问题”。
@@ -11,37 +11,39 @@

例题中已知条件有第 $i$ 个物品的重量 $w_{i}$,价值 $v_{i}$,以及背包的总容量 $W$。

设 DP 状态 $f_{i,W}$ 为在只能放前 $i$ 个物品的情况下,容量为 $W$ 的背包所能达到的最大总价值。
设 DP 状态 $f_{i,j}$ 为在只能放前 $i$ 个物品的情况下,容量为 $j$ 的背包所能达到的最大总价值。

考虑转移。假设当前已经处理好了前 $i-1$ 个物品的所有状态,那么对于第 $i$ 个物品,当其不放入背包时,背包的剩余容量不变,背包中物品的总价值也不变,故这种情况的最大价值为 $f_{i-1,W}$;当其放入背包时,背包的剩余容量会减小 $w_{i}$,背包中物品的总价值会增大 $v_{i}$,故这种情况的最大价值为 $f_{i-1,W-w_{i}}+v_{i}$。
考虑转移。假设当前已经处理好了前 $i-1$ 个物品的所有状态,那么对于第 $i$ 个物品,当其不放入背包时,背包的剩余容量不变,背包中物品的总价值也不变,故这种情况的最大价值为 $f_{i-1,j}$;当其放入背包时,背包的剩余容量会减小 $w_{i}$,背包中物品的总价值会增大 $v_{i}$,故这种情况的最大价值为 $f_{i-1,j-w_{i}}+v_{i}$。

由此可以得出状态转移方程:

$$f_{i,W}=\max(f_{i-1,W},f_{i-1,W-w_{i}}+v_{i})$$
$$
f_{i,j}=\max(f_{i-1,j},f_{i-1,j-w_{i}}+v_{i})
$$

在程序实现的时候,由于对当前状态有影响的只有 $f_{i-1}$,故可以去掉第一维,直接用 $dp_{W}$ 来表示处理到当前物品 $i$ 时背包容量为 $W$ 的最大价值 $f_{i,W}$。
在程序实现的时候,由于对当前状态有影响的只有 $f_{i-1}$,故可以去掉第一维,直接用 $f_{j}$ 来表示处理到当前物品 $i$ 时背包容量为 $j$ 的最大价值 $f_{i,j}$。

还有一点需要注意的是,很容易写出这样的错误核心代码:

```cpp
for (int i = 1; i <= W; i++)
  for (int l = 0; l <= W - i; l++)
    dp[l + w[i]] = max(dp[l] + v[i], dp[l + w[i]]);
    f[l + w[i]] = max(f[l] + v[i], f[l + w[i]]);
    // 由 f[i][l + w[i]] = max(max(f[i - 1][l + w[i]],f[i - 1][l] + w[i]),f[i][l + w[i]]); 简化而来
```

这段代码哪里错了呢?枚举顺序错了。

仔细观察代码可以发现:对于当前处理的物品 $i$ 和当前状态 $f_{i,W}$,在 $W\geqslant w_{i}$ 时,$f_{i,W}$ 是会被 $f_{i,W-w_{i}}$ 所影响的。这就相当于物品 $i$ 可以多次被放入背包,与题意不符。(事实上,这正是完全背包问题的解法)
仔细观察代码可以发现:对于当前处理的物品 $i$ 和当前状态 $f_{i,j}$,在 $j\geqslant w_{i}$ 时,$f_{i,j}$ 是会被 $f_{i,j-w_{i}}$ 所影响的。这就相当于物品 $i$ 可以多次被放入背包,与题意不符。(事实上,这正是完全背包问题的解法)

为了避免这种情况发生,我们可以改变枚举的顺序,从 $W$ 枚举到 $w_{i}$,这样就不会出现上述的错误,因为 $f_{i,W}$ 总是在 $f_{i,W-w_{i}}$ 前被更新。
为了避免这种情况发生,我们可以改变枚举的顺序,从 $W$ 枚举到 $w_{i}$,这样就不会出现上述的错误,因为 $f_{i,j}$ 总是在 $f_{i,j-w_{i}}$ 前被更新。

因此实际核心代码为

```cpp
for (int i = 1; i <= W; i++)
  for (int l = W - i; l >= 0; l--)
    dp[l + i] = max(dp[l] + w[i], dp[l + i]);
    f[l + i] = max(f[l] + w[i], f[l + i]);
    // 由 f[i][l + w[i]] = max(max(f[i - 1][l + w[i]],f[i - 1][l] + w[i]),f[i][l + w[i]]); 简化而来
```

@@ -50,20 +52,68 @@ for (int i = 1; i <= W; i++)
    ```cpp
    #include <iostream>
    const int maxn = 13010;
    int n, v, w[maxn], v[maxn], dp[maxn];
    int n, v, w[maxn], v[maxn], f[maxn];
    int main() {
        std::cin >> n >> W;
        for (int i = 1; i <= n; i++)
            std::cin >> w[i] >> v[i];
        for (int i = 1; i <= n; i++)
            for (int l = W; l >= w[i]; l--)
          if (dp[l - w[i]] + v[i] > dp[l])
            dp[l] = dp[l - w[i]] + v[i];
      std::cout << dp[W];
                if (f[l - w[i]] + v[i] > f[l])
                    f[l] = f[l - w[i]] + v[i];
        std::cout << f[W];
        return 0;
    }
    ```

## 完全背包

完全背包模型与 0-1 背包类似,与 0-1 背包的区别仅在于一个物品可以选取无限次,而非仅能选取一次。

我们可以借鉴 0-1 背包的思路,进行状态定义:设 $f_{i,j}$ 为只能选前 $i$ 个物品时,容量为 $j$ 的背包可以达到的最大价值。

需要注意的是,虽然定义与 0-1 背包类似,但是其状态转移方程与 0-1 背包并不相同。

可以考虑一个朴素的做法:对于第 $i$ 件物品,枚举其选了多少个来转移。这样做的时间复杂度是 $O(n^3)$ 的。

尽管这样看起来很蠢,我们还是写一下 dp 方程:

$$
f_{i,j}=\max_{k=0}^{+\infty}(f_{i-1,j-k\times w_i}+v_i\times k)
$$

然而这样显然不够优秀,我们要对它进行优化。

可以发现,对于 $f_{i,j}$,只要通过 $f_{i,j-w_i}$ 转移就可以了。dp 方程为:

$$
f_{i,j}=\max(f_{i-1,j},f_{i,j-w_i}+v_i)
$$

理由是当我们这样转移时,$f_{i,j-w_i}$ 已经由 $f_{i,j-2\times w_i}$ 更新过,那么 $f_{i,j-w_i}$ 就是充分考虑了第 $i$ 件物品所选次数后得到的最优结果。换言之,我们通过局部最优子结构的性质重复使用了之前的枚举过程,优化了枚举的复杂度。

与 0-1 背包相同地,我们可以将第一维去掉来优化空间复杂度。如果理解了 0-1 背包的优化方式,就不难明白压缩后的循环是正向的(也就是上文中提到的错误优化)。

??? note " [「Luogu P1616」疯狂的采药](<https://www.luogu.org/problemnew/show/P1616>)"
    题意概要:有 $n$ 种物品和一个容量为 $W$ 的背包,每种物品有重量 $w_{i}$ 和价值 $v_{i}$ 两种属性,要求选若干个物品放入背包使背包中物品的总价值最大且背包中物品的总重量不超过背包的容量。

??? 例题代码

    ```cpp
    #include <iostream>
    const int maxn = 13010;
    int n, v, w[maxn], v[maxn], f[maxn];
    int main() {
        std::cin >> n >> W;
        for (int i = 1; i <= n; i++)
            std::cin >> w[i] >> v[i];
        for (int i = 1; i <= n; i++)
            for (int l = w[i]; l <= W; l++)
                if (f[l - w[i]] + v[i] > f[l])
                    f[l] = f[l - w[i]] + v[i];
        std::cout << f[W];
        return 0;
    }
    ```

## 多重背包
+5 −3
Original line number Diff line number Diff line
@@ -29,7 +29,7 @@ struct Node_t {
  int l, r;
  mutable int v;
  Node_t(const int &il, const int &ir, const int &iv) : l(il), r(ir), v(iv) {}
  inline bool operator(const Node_t &o) const { return l < o.l; }
  inline bool operator<(const Node_t &o) const { return l < o.l; }
};
```

@@ -40,7 +40,7 @@ struct Node_t {

### split

最核心的操作之一 `split` ,它用于取得以 $x$ 开头的结点
`split` 是最核心的操作之一,它用于将原本包含点 $x$ 的区间(设为 $[l, r]$)分裂为两个区间 $[l, x)$ 和 $[x, r]$ 并返回指向后者的迭代器
参考代码如下:

```cpp
@@ -87,6 +87,8 @@ void performance(int l, int r) {
}
```

**注:珂朵莉树在进行求取区间左右端点操作时,必须先 split 右端点,再 split 左端点。否则在处理边界情况时会导致 RE。**

## 习题

-   [「SCOI2010」序列操作](https://www.lydsy.com/JudgeOnline/problem.php?id=1858)
+1 −1
Original line number Diff line number Diff line
@@ -18,7 +18,7 @@ __gnu_pbds ::priority_queue<T, Compare, Tag, Allocator>
    -    `binary_heap_tag` :二叉堆 
        官方文档认为在原生元素中二叉堆表现最好,不过我测试的表现并没有那么好
    -    `binomial_heap_tag` :二项堆
        二项堆在合并操作的表现要优于配对堆\*但是其取堆顶元素
        二项堆在合并操作的表现要优于二叉堆,但是其取堆顶元素操作的复杂度比二叉堆高。
    -    `rc_binomial_heap_tag` :冗余计数二项堆
    -    `thin_heap_tag` :除了合并的复杂度都和 Fibonacci 堆一样的一个 tag
-    `Allocator` :空间配置器,由于 OI 中很少出现,故这里不做讲解
+1 −1
Original line number Diff line number Diff line
@@ -67,8 +67,8 @@ void rotate(int x) {
  fa[y] = x;
  fa[x] = z;
  if (z) ch[z][y == ch[z][1]] = x;
  maintain(x);
  maintain(y);
  maintain(x);
}
```

+3 −3
Original line number Diff line number Diff line
@@ -41,7 +41,7 @@ IOI(International Olympiad in Informatics)是国际信息学奥林匹克竞
#### PKU

-   北京大学信息学冬季体验营(PKUWC)在冬令营前后举行。
-   北京大学信息学体验营(PKUSC)一般在六月份在校内举行。由于在学校机房比赛,机房环境是 windows,比赛系统是 openjudge。
-   北京大学信息学体验营(PKUSC)一般在六月份在校内举行。由于在学校机房比赛,机房环境是 Windows,比赛系统是 OpenJudge。
-   北京大学中学生暑期课堂(信息学)在暑假举行,面向高二年级理科学生。

## 赛制介绍
Loading