背包问题

01 背包 #

给定 $n$ 个物品,第 $i$ 个物品价值为 $c[i]$, 体积为 $w[i]$. 现有容积为 $m$ 的背包,求将物品装入背包得到的最大价值.

$f[i,v]$: 从前 $i$ 个物品中,选出总体积为 $v$ 的物品,能得到的最大价值.

  • 不选第 $i$ 个物品:$f[i,v]=f[i-1,v]$.
  • 选第 $i$ 个物品:$f[i,v]=f[i-1,v-w_i]+c_i$.

$$f[i,v] = \max\left\{\begin{aligned} &f[i-1,v]\\ &f[i-1,v-w_i]+c_i\quad(v≥w_i)\\ \end{aligned}\right.$$

时间复杂度:$O(nm)$.

for (int i = 1; i <= n; i ++) {
    for (int v = 0; v <= m; v ++) {
        f[i][v] = f[i - 1][v];
        if (v >= w[i])
            f[i][v] = max(f[i][v], f[i - 1][v - w[i]] + c[i]);
    }
}
// 问题的解是 f[n][m]

空间优化 #

实际上,状态转移方程的第一维可以去掉,即让新状态覆盖旧状态,降低空间开销.

$$f[v] = \max\left\{\begin{aligned} &f[v]\\ &f[v-w_i]+c_i\quad(v≥w_i) \end{aligned}\right.$$

但在程序中,直接移除第一维会导致错误. 令 $w=1,c=2$,执行内层循环,可见错误的根源.

在内层循环中,$f[v-w]$ 不能比 $f[v]$ 先更新,否则相当于同一物品被装多次. 倒序枚举 $v$ 以解决问题.

$v$ $0$ $1$ $2$ $3$ $4$
$f[v]$ $0$ $0$ $0$ $\textcolor{blue}{0}$ $\textcolor{blue}{f[v-w]}+c=2$
$v$ $0$ $1$ $2$ $3$ $4$
$f[v]$ $0$ $0$ $\textcolor{blue}{0}$ $\textcolor{blue}{f[v-w]}+c=2$ $2$
$v$ $0$ $1$ $2$ $3$ $4$
$f[v]$ $0$ $\textcolor{blue}{0}$ $\textcolor{blue}{f[v-w]}+c=2$ $2$ $2$
$v$ $0$ $1$ $2$ $3$ $4$
$f[v]$ $\textcolor{blue}{0}$ $\textcolor{blue}{f[v-w]}+c=2$ $2$ $2$ $2$
for (int i = 1; i <= n; i ++)
    for (int v = m; v >= w[i]; v --)    // 倒序枚举
        f[v] = max(f[v], f[v - w[i]] + c[i]);

完全背包 #

基于 01 背包,每种物品可装无限次.

$f[i,v]$: 从前 $i$ 种物品中,选出总体积为 $v$ 的物品,能得到的最大价值.

  • 不选第 $i$ 种物品:$f[i,v]=f[i-1,v]$.
  • 多选一个第 $i$ 种物品:$f[i,v]=f[i,v-w_i]+c_i$.

$$f[i,v] = \max\left\{\begin{aligned} &f[i-1,v]\\ &f[i,v-w_i]+c_i\quad(v≥w_i) \end{aligned}\right.$$

时间复杂度:$O(nm)$.

for (int i = 1; i <= n; i ++) {
    for (int v = 0; v < m; v ++) {
        f[i][v] = f[i - 1][v];
        if (v >= w[i])
            f[i][v] = max(f[i][v], f[i][v - w[i]] + c[i]);
    }
}

空间优化 #

参照 01 背包 - 空间优化 中的错误做法:直接移除第一维,并升序枚举 $v$,相当于将同种物品装多次.

$v$ $0$ $1$ $2$ $3$ $4$
$f[v]$ $\textcolor{blue}{0}$ $\textcolor{blue}{f[v-w]}+c=2$ $0$ $0$ $0$
$v$ $0$ $1$ $2$ $3$ $4$
$f[v]$ $0$ $\textcolor{blue}{2}$ $\textcolor{blue}{f[v-w]}+c=4$ $0$ $0$
$v$ $0$ $1$ $2$ $3$ $4$
$f[v]$ $0$ $2$ $\textcolor{blue}{4}$ $\textcolor{blue}{f[v-w]}+c=6$ $0$
$v$ $0$ $1$ $2$ $3$ $4$
$f[v]$ $0$ $2$ $4$ $\textcolor{blue}{6}$ $\textcolor{blue}{f[v-w]}+c=8$
for (int i = 1; i <= n; i ++)
    for (int v = w[i]; v <= m; v ++)
        f[v] = max(f[v], f[v - w[i]] + c[i]);

多重背包 #

基于 01 背包,第 $i$ 种物品可装 $s_i$ 次.

在内层循环中,分别绑定 $1,2,\cdots s_i$ 个物品作为一个物品,参与 01 背包.

$$f[v] = \max_{k=1,2,\cdots,s_i}\begin{cases} f[v]\\ f[v-k\cdot w_i]+k\cdot c_i\quad(v≥k\cdot w_i) \end{cases}$$

时间复杂度:$O(m\sum s_i)$.

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

二进制优化 #

  • 任意正整数 $n$ 可拆分为 $1,2,4,\cdots,2^k,n-2^k$($n-2^k≥0$ 且 $k$ 尽量大).
  • 相反,$1,2,4,\cdots,2^k,n-2^k$ 能且仅能组合出 $[1,n]$ 中的所有整数.

将 $s_i$ 个物品拆分为 $1,2,4,\cdots,2^k,s_i-2^k$ 个物品(共 $\log s_i$ 组),并绑定每组为一个物品,参与 01 背包. 例如,$s_i=15$ 可以拆分为以下 $4$ 组:

组别 $1$ $2$ $3$ $4$
总体积 $w_i$ $2w_i$ $4w_i$ $8w_i$
总价值 $c_i$ $2c_i$ $4c_i$ $8c_i$

此 $4$ 组物品可组合出所有策略(选 $1\cdots 15$ 个物品). 例如要选 $12$ 个物品,则同时选第 $3,4$ 组即可.

时间复杂度:$O(m\sum\log{s_i})$.

for (int i = 1; i <= n; i ++) {
    int wi, ci, s;
    cin >> wi >> ci >> s;
    for (int k = 1; k <= s; k *= 2) {
        w[++ cnt] = k * wi, c[cnt] = k * ci;
        s -= k;
    }
    if (s)
        w[++ cnt] = s * wi, c[cnt] = s * ci;
}
for (int i = 1; i <= tot; i ++)
    for (int v = m; v >= w[i]; v --)
        f[v] = max(f[v], f[v - w[i]] + c[i]);

分组背包 #

背包体积为 $m$,有 $t$ 组物品,第 $k$ 组有 $s_k$ 个,其中第 $i$ 个体积为 $w_{ki}$,价值为 $c_{ki}$. 每组只能取走一个物品. 求将物品装入背包得到的最大价值.

$f[k,v]$:从前 $k$ 组物品中,选出总体积为 $v$ 的物品,能得到的最大价值.

  • 不选第 $k$ 组:$f[k,v]=f[k-1,v]$.
  • 选第 $k$ 组:$ f[k,v]=\max_{1\leq i\leq s_k}\ f[k-1,v-w_{ki}]+c_{ki}$.

$$f[k,v] = \max\left\{\begin{aligned} &f[k-1,v]\\ &\max_{1\leq i\leq s_k}\ f[k-1,v-w_{ki}]+c_{ki}\quad(v≥w_{ki}) \end{aligned}\right.$$

可以省去第一维. 时间复杂度:$O(nm)$.

for (int k = 1; k <= t; k ++)
	for (int v = m; v >= 0; v --)
		for (int i = 1; i <= s[k]; i ++)
			if (v >= w[k][i])
				f[v] = max(f[v],f[v - w[k][i]] + c[k][i]);

二维背包 #

背包体积为 $V$,承重为 $M$. 有 $n$ 个物品,第 $i$ 个体积为 $v_i$,质量为 $m_i$,价值为 $c_i$. 求将物品装入背包得到的最大价值.

$f[p,q]$:用体积为 $p$,承重为 $q$ 的背包放物品,能获得的最大总价值.

$$f[p,q] = \max\left\{\begin{aligned} &f[p,q]\\ &f[p-v_i,q-m_i]+c_i\quad(p≥v_i,q≥m_i) \end{aligned}\right.$$

时间复杂度为 $O(nVM)$.

for (int i = 1; i <= n; i ++)
    for (int p = V; p >= v[i]; p --)
        for (int q = M; q >= m[i]; q --)
            f[p][q] = max(f[p][q], f[p - v[i]][q - m[i]] + c[i]);

方案总数 #

基于 01 背包,求将物品放入背包的方案数.

$g[i,v]$:把前 $i$ 个物品(部分或全部)放入体积为 $v$ 的背包的方案总数.

  • 不放第 $i$ 个物品:有 $g[i-1,v]$ 种
  • 放第 $i$ 个物品:有 $g[i-1,v-w_i]$ 种

$$g[i,v]=g[i-1,v]+g[i-1,v-w_i]$$

同样可以省去第一维:

$$g[v]=g[v]+g[v-w_i]\quad(v≥w_i)$$

初始条件:$g[0]=1$. 因为当背包体积为 $0$ 时只有一个方案:不放任何物品.

g[0] = 1;
for (int i = 1; i <= n; i ++)
    for (int v = m; v >= w[i]; v --)
        g[v] += g[v - w[i]];

最优方案总数 #

基于 01 背包,求最优方案数.

已知 01 背包 的状态转移方程:

$$f[i,v] = \max\left\{\begin{aligned} &f[i-1,v]\\ &f[i-1,v-w_i]+c_i\quad(v≥w_i)\\ \end{aligned}\right.$$

$g[i,v]$:把前 $i$ 个物品(部分或全部)放入体积为 $v$ 的背包的最优方案数.

  • 若 $f[i-1,v]>f[i-1,v-w_i]+c_i$,不放物品 $i$ 最优:$g[i,v]=g[i-1,v]$.

  • 若 $f[i-1,v]<f[i-1,v-w_i]+c_i$,放物品 $i$ 最优:$g[i,v]=g[i-1,v-w_i]$.

  • 若相等,则两种方案都最优:$g[i,v]=g[i-1,v]+g[i-1,v-w_i]$.

for (int i = 1; i <= n; i ++) {
    for (int v = 0; v <= m; v ++) {
        f[i][v] = f[i - 1][v];
        if (v >= w[i])
            f[i][v] = max(f[i][v], f[i - 1][v - w[i]] + c[i]);
        if (f[i][v] == f[i - 1][v])
            g[i][v] += g[i - 1][v];
        if (f[i][v] == f[i - 1][v - w[i]] + c[i])
            g[i][v] += g[v - w[i]];
    }
}

省去第一维:

for (int i = 1; i <= n; i ++) {
    for (int v = m; v >= w[i]; v --) {
        int maxn = max(f[v], f[v - w[i]] + c[i]);
        int maxg = 0; // 最优方案数
        if(maxn == f[v])
            maxg += g[v];
        if(maxn == f[v - w[i]] + c[i])
            maxg += g[v - w[i]];
        f[v] = maxn, g[v] = maxg;
    }
}