背包问题

推公式的瘾犯了,才有了这篇题解

01 背包

问题简述

给你 NNN \in \mathbb{N} 个物品,其中第 ii 个物品具有体积 viNv_i \in \mathbb{N} 和价值 wiNw_i \in \mathbb{N},和一个容量为 VNV \in \mathbb{N} 的背包。现在从 NN 个物品中选出一些物品,要求在能放入背包的前提下(即选出的物品的体积总和小于等于背包容量)使得选出物品的价值总和最大。

符号约定

首先我们用 concat\text{concat} 函数表示两个序列 (an)(a_n)(bm)(b_m) 的拼接:

concat:Na×NbNa+bconcat(an,bm)=(a1,a2,,an,b1,b2,,bm)\large \text{concat} : \mathbb{N}^a \times \mathbb{N}^b \rightarrow \mathbb{N}^{a+b} \quad \text{concat}(a_n, b_m) = (a_1, a_2, \cdots, a_n, b_1, b_2, \cdots, b_m)

我们用一个严格递增的序列:

ok=(ok,s)s{1,2,,t}=(ok,1,ok,2,,ok,t)\large o_{k} = (o_{k, s})_{s \in \{1, 2, \cdots, t\}} = (o_{k,1}, o_{k,2}, \cdots, o_{k, t})

表示从前 k{1,2,,N}k \in \{1, 2, \cdots, N\} 个物品中选择 t{0,1,,k}t \in \{0, 1,\cdots, k\} 个物品的一种选法,其中 1ok,sk, ok,1<ok,2<<ok,t1 \leqslant o_{k, s} \leqslant k, \ o_{k, 1} < o_{k, 2} < \cdots < o_{k, t} 表示在这种选法中,被选中的第 ss 个物品是原来第 ok,so_{k, s} 个物品。

用一个序列的集合:

Ok={ok | ok=(ok,1,ok,2,,ok,t), 1ok,sk, ok,1<ok,2<<ok,t}\large O_k = \left\{o_k \ \middle| \ o_{k} = (o_{k,1}, o_{k,2}, \cdots, o_{k, t}), \ 1 \leqslant o_{k, s} \leqslant k, \ o_{k, 1} < o_{k, 2} < \cdots < o_{k, t} \right\}

表示所有合法 oko_k 的集合。

用一个序列的集合:

Sij={oi | s=1tvoi,sj}\large S_{ij} = \left\{o_i \ \middle| \ \sum_{s = 1}^t v_{o_{i,s}} \leqslant j\right\}

表示从前 ii 个物品中选,且体积总和小于等于 jj 的所有选法集合。

用集合的集合:

S={Sij | i{1,2,,N}, j{0,1,,V}}\large \mathcal{S} = \left\{S_{ij} \ \middle| \ i \in \{1, 2, \cdots, N\}, \ j \in \{0, 1, \cdots, V\} \right\}

把所有合法的 SijS_{ij} 打包为一个大集合。

用一个函数:

g:SNg(Sij)=maxoiSijs=1twoi,s\large g: \mathcal{S} \rightarrow \mathbb{N} \quad g(S_{ij}) = \max_{o_i \in S_{ij}} \sum_{s=1}^t w_{o_{i,s}}

表示从前 ii 个物品中选,且体积总和小于等于 jj 的所有选法中选中的物品价值和的最大值。

设二元函数:

f:N×NNf(i,j)=g(Sij)\large f: \mathbb{N} \times \mathbb{N} \rightarrow \mathbb{N} \quad f(i, j) = g(S_{ij})

算法分析

考虑拆分问题,对于第 kk 个物品只有选或不选两种选择。设序列 ok1=(ok1,1,ok1,2,,ok1,t)o_{k - 1} = (o_{k-1, 1}, o_{k-1,2}, \cdots, o_{k-1,t}) 表示从前 k1k - 1 个物品中选择的某一种选法。现在考虑第 kk 个物品,如果选择了第 kk 个物品,那么序列 ok=(ok1,1,ok1,2,,ok1,t,k)o_k = (o_{k-1, 1}, o_{k-1,2}, \cdots, o_{k-1,t}, k) 将会是一个从前 kk 个物品中选择的一个选法,如果不选第 kk 个物品,那么序列 ok=(ok1,1,ok1,2,,ok1,t)=ok1o'_k = (o_{k-1, 1}, o_{k-1,2}, \cdots, o_{k-1,t}) = o_{k - 1} 将会是一个从前 kk 个物品中选择的一个选法。于是可以选择使用动态规划,把大问题分解成多个小问题,在计算完所有小问题的答案之后,汇总这些答案,得到大问题的答案。

现在考虑如何计算 g(Sij)g(S_{ij}). 如果我们能够把 SijS_{ij} 划分为 uu 个不相交的子集 A1,A2,,AuA_1, A_2, \cdots, A_u,即 pqApAq=p \neq q \rightarrow A_p \cap A_q = \emptyset 其中 p,q{1,2,,u}p, q \in \{1, 2, \cdots, u\},使得 A1A2Au=SijA_1 \cup A_2 \cup \cdots \cup A_u = S_{ij},那么我们就能轻松地计算出:

g(Sij)=maxp{1,2,,u}g(Ap)g(S_{ij}) = \max_{p \in \{1, 2, \cdots, u\}}g(A_p)

根据刚刚的拆分思路,我们将 SijS_{ij} 按照第 ii 个物品有没有被选中拆分成两个集合:

A1={oi | oi,t=i}, A2={oi | oi,ti}A_1 = \left\{o_i \ \middle| \ o_{i,t} = i\right\}, \ A_2 = \left\{o_i \ \middle| \ o_{i,t} \neq i\right\}

A1A_1 表示所有选中了第 ii 个物品的选法集合,A2A_2 表示所有没有选中第 ii 个物品的选法集合。由于 A2A_2 中的选法只从前 i1i - 1 个物品中选择,有:

A2=Si1,jA_2 = S_{i-1, j}

所以

g(A2)=g(Si1,j)g(A_2) = g(S_{i-1, j})

观察 A1A_1 的元素 oio_ioio_i 形如 (oi,1,oi,2,,oi,t1,i)(o_{i,1}, o_{i,2}, \cdots, o_{i, t-1}, i). 这个序列的前 t1t - 1 项不会出现 ii,即前 t1t - 1 项只会从前 i1i - 1 个物品中选择,所以 (oi,1,oi,2,,oi,t1)Oi1(o_{i,1}, o_{i,2}, \cdots, o_{i, t-1}) \in O_{i - 1}. 由于 SijS_{ij} 总体积和小于等于 jj 的要求:

s=1tvoi,sj\large \sum_{s = 1}^t v_{o_{i,s}} \leqslant j

对于任意一个 A1A_1 的元素 oi=(oi,1,oi,2,,oi,t1,i)o_i = (o_{i,1}, o_{i,2}, \cdots, o_{i, t-1}, i) 有:

s=1tvoi,s=s=1t1voi,s+vij\large \sum_{s = 1}^t v_{o_{i,s}} = \sum_{s = 1}^{t - 1} v_{o_{i,s}} + v_i\leqslant j

s=1t1voi,sjvi\large \sum_{s = 1}^{t - 1} v_{o_{i,s}}\leqslant j - v_i

即除去最后一个 ii 物品,前 t1t - 1 个选中的物品的体积和满足小于等于 jvij - v_i. 再加上刚刚得到的 (oi,1,oi,2,,oi,t1)Oi1(o_{i,1}, o_{i,2}, \cdots, o_{i, t-1}) \in O_{i - 1},我们有 (oi,1,oi,2,,oi,t1)Si1,jvi(o_{i,1}, o_{i,2}, \cdots, o_{i, t-1}) \in S_{i - 1, j - v_i}. 即除去最后一个 ii 物品的子序列是一个在前 i1i - 1 个物品中选,体积和不超过 jvij - v_i 的选法序列。反过来,显然任意一个 Si1,jviS_{i-1, j-v_i} 中的序列 oi1o_{i-1},在末尾拼接上 ii 之后,就成为 A1A_1 中的某个序列 oio_i,即 concat(oi1,(i))A1\text{concat}(o_{i-1}, (i)) \in A_1。所以:

g(A1)=g(Si1,jvi)+wig(A_1) = g(S_{i - 1, j - v_i}) + w_i

所以

g(Sij)=max(g(Si1,jvi+wi), g(Si1,j))g(S_{ij}) = \max(g(S_{i - 1, j - v_i} + w_i), \ g(S_{i - 1, j}))

f(i,j)=max(f(i1,jvi)+wi, f(i1,j))f(i, j) = \max(f(i - 1, j - v_i) + w_i, \ f(i - 1, j))

朴素的实现

注意到只有当 jvij \geqslant v_i 时才有 A1A_1 \neq \emptyset.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const int N = 1010;

int f[N][N];
int v[N], w[N];

int main() {
int n, V;
cin >> n >> V;

for (int i = 1; i <= n; i++) {
cin >> v[i] >> w[i];
}

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

优化的实现

由于 f(i,j)f(i, j) 的计算只用到了 f(i1,)f(i - 1, *),只需要保存前一轮次的计算结果 f(i1,)f(i - 1, *). 于是可以省去 f 数组的第一个维度。注意到 f[j - v[i]] 在数组中的位置比 f[j] 靠前,如果正着枚举 j 会导致计算 f[j]f[j - v[i]] 已经被更新,即 f[j - v[i]] 等于 f(i,jvi)f(i, j - v_i) 而不是我们需要的 f(i1,jvi)f(i - 1, j - v_i). 所以枚举第二维 j 的时候需要倒着枚举。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int f[N];
int v[N], w[N];

int main() {
int n, V;
cin >> n >> V;

for (int i = 1; i <= n; i++) {
cin >> v[i] >> w[i];
}

for (int i = 1; i <= n; i++) {
for (int j = V; j >= v[i]; j--) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[V] << endl;
}

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!