推公式的瘾犯了,才有了这篇题解
01 背包
问题简述
给你 N ∈ N N \in \mathbb{N} N ∈ N 个物品,其中第 i i i 个物品具有体积 v i ∈ N v_i \in \mathbb{N} v i ∈ N 和价值 w i ∈ N w_i \in \mathbb{N} w i ∈ N ,和一个容量为 V ∈ N V \in \mathbb{N} V ∈ N 的背包。现在从 N N N 个物品中选出一些物品,要求在能放入背包的前提下(即选出的物品的体积总和小于等于背包容量)使得选出物品的价值总和最大。
符号约定
首先我们用 concat \text{concat} concat 函数表示两个序列 ( a n ) (a_n) ( a n ) 和 ( b m ) (b_m) ( b m ) 的拼接:
concat : N a × N b → N a + b concat ( a n , b m ) = ( a 1 , a 2 , ⋯ , a n , b 1 , b 2 , ⋯ , b m ) \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)
concat : N a × N b → N a + b concat ( a n , b m ) = ( a 1 , a 2 , ⋯ , a n , b 1 , b 2 , ⋯ , b m )
我们用一个严格递增的序列:
o k = ( o k , s ) s ∈ { 1 , 2 , ⋯ , t } = ( o k , 1 , o k , 2 , ⋯ , o k , t ) \large
o_{k} = (o_{k, s})_{s \in \{1, 2, \cdots, t\}} = (o_{k,1}, o_{k,2}, \cdots, o_{k, t})
o k = ( o k , s ) s ∈ { 1 , 2 , ⋯ , t } = ( o k , 1 , o k , 2 , ⋯ , o k , t )
表示从前 k ∈ { 1 , 2 , ⋯ , N } k \in \{1, 2, \cdots, N\} k ∈ { 1 , 2 , ⋯ , N } 个物品中选择 t ∈ { 0 , 1 , ⋯ , k } t \in \{0, 1,\cdots, k\} t ∈ { 0 , 1 , ⋯ , k } 个物品的一种选法,其中 1 ⩽ o k , s ⩽ k , o k , 1 < o k , 2 < ⋯ < o k , t 1 \leqslant o_{k, s} \leqslant k, \ o_{k, 1} < o_{k, 2} < \cdots < o_{k, t} 1 ⩽ o k , s ⩽ k , o k , 1 < o k , 2 < ⋯ < o k , t 表示在这种选法中,被选中的第 s s s 个物品是原来第 o k , s o_{k, s} o k , s 个物品。
用一个序列的集合:
O k = { o k | o k = ( o k , 1 , o k , 2 , ⋯ , o k , t ) , 1 ⩽ o k , s ⩽ k , o k , 1 < o k , 2 < ⋯ < o k , 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\}
O k = { o k o k = ( o k , 1 , o k , 2 , ⋯ , o k , t ) , 1 ⩽ o k , s ⩽ k , o k , 1 < o k , 2 < ⋯ < o k , t }
表示所有合法 o k o_k o k 的集合。
用一个序列的集合:
S i j = { o i | ∑ s = 1 t v o i , s ⩽ j } \large
S_{ij} = \left\{o_i \ \middle| \ \sum_{s = 1}^t v_{o_{i,s}} \leqslant j\right\}
S ij = ⎩ ⎨ ⎧ o i s = 1 ∑ t v o i , s ⩽ j ⎭ ⎬ ⎫
表示从前 i i i 个物品中选,且体积总和小于等于 j j j 的所有选法集合。
用集合的集合:
S = { S i j | 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\}
S = { S ij i ∈ { 1 , 2 , ⋯ , N } , j ∈ { 0 , 1 , ⋯ , V } }
把所有合法的 S i j S_{ij} S ij 打包为一个大集合。
用一个函数:
g : S → N g ( S i j ) = max o i ∈ S i j ∑ s = 1 t w o i , 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}}
g : S → N g ( S ij ) = o i ∈ S ij max s = 1 ∑ t w o i , s
表示从前 i i i 个物品中选,且体积总和小于等于 j j j 的所有选法中选中的物品价值和的最大值。
设二元函数:
f : N × N → N f ( i , j ) = g ( S i j ) \large
f: \mathbb{N} \times \mathbb{N} \rightarrow \mathbb{N} \quad f(i, j) = g(S_{ij})
f : N × N → N f ( i , j ) = g ( S ij )
算法分析
考虑拆分问题,对于第 k k k 个物品只有选或不选两种选择。设序列 o k − 1 = ( o k − 1 , 1 , o k − 1 , 2 , ⋯ , o k − 1 , t ) o_{k - 1} = (o_{k-1, 1}, o_{k-1,2}, \cdots, o_{k-1,t}) o k − 1 = ( o k − 1 , 1 , o k − 1 , 2 , ⋯ , o k − 1 , t ) 表示从前 k − 1 k - 1 k − 1 个物品中选择的某一种选法。现在考虑第 k k k 个物品,如果选择了第 k k k 个物品,那么序列 o k = ( o k − 1 , 1 , o k − 1 , 2 , ⋯ , o k − 1 , t , k ) o_k = (o_{k-1, 1}, o_{k-1,2}, \cdots, o_{k-1,t}, k) o k = ( o k − 1 , 1 , o k − 1 , 2 , ⋯ , o k − 1 , t , k ) 将会是一个从前 k k k 个物品中选择的一个选法,如果不选第 k k k 个物品,那么序列 o k ′ = ( o k − 1 , 1 , o k − 1 , 2 , ⋯ , o k − 1 , t ) = o k − 1 o'_k = (o_{k-1, 1}, o_{k-1,2}, \cdots, o_{k-1,t}) = o_{k - 1} o k ′ = ( o k − 1 , 1 , o k − 1 , 2 , ⋯ , o k − 1 , t ) = o k − 1 将会是一个从前 k k k 个物品中选择的一个选法。于是可以选择使用动态规划,把大问题分解成多个小问题,在计算完所有小问题的答案之后,汇总这些答案,得到大问题的答案。
现在考虑如何计算 g ( S i j ) g(S_{ij}) g ( S ij ) . 如果我们能够把 S i j S_{ij} S ij 划分为 u u u 个不相交的子集 A 1 , A 2 , ⋯ , A u A_1, A_2, \cdots, A_u A 1 , A 2 , ⋯ , A u ,即 p ≠ q → A p ∩ A q = ∅ p \neq q \rightarrow A_p \cap A_q = \emptyset p = q → A p ∩ A q = ∅ 其中 p , q ∈ { 1 , 2 , ⋯ , u } p, q \in \{1, 2, \cdots, u\} p , q ∈ { 1 , 2 , ⋯ , u } ,使得 A 1 ∪ A 2 ∪ ⋯ ∪ A u = S i j A_1 \cup A_2 \cup \cdots \cup A_u = S_{ij} A 1 ∪ A 2 ∪ ⋯ ∪ A u = S ij ,那么我们就能轻松地计算出:
g ( S i j ) = max p ∈ { 1 , 2 , ⋯ , u } g ( A p ) g(S_{ij}) = \max_{p \in \{1, 2, \cdots, u\}}g(A_p)
g ( S ij ) = p ∈ { 1 , 2 , ⋯ , u } max g ( A p )
根据刚刚的拆分思路,我们将 S i j S_{ij} S ij 按照第 i i i 个物品有没有被选中拆分成两个集合:
A 1 = { o i | o i , t = i } , A 2 = { o i | o i , t ≠ i } A_1 = \left\{o_i \ \middle| \ o_{i,t} = i\right\}, \ A_2 = \left\{o_i \ \middle| \ o_{i,t} \neq i\right\}
A 1 = { o i ∣ o i , t = i } , A 2 = { o i ∣ o i , t = i }
A 1 A_1 A 1 表示所有选中了第 i i i 个物品的选法集合,A 2 A_2 A 2 表示所有没有选中第 i i i 个物品的选法集合。由于 A 2 A_2 A 2 中的选法只从前 i − 1 i - 1 i − 1 个物品中选择,有:
A 2 = S i − 1 , j A_2 = S_{i-1, j}
A 2 = S i − 1 , j
所以
g ( A 2 ) = g ( S i − 1 , j ) g(A_2) = g(S_{i-1, j})
g ( A 2 ) = g ( S i − 1 , j )
观察 A 1 A_1 A 1 的元素 o i o_i o i ,o i o_i o i 形如 ( o i , 1 , o i , 2 , ⋯ , o i , t − 1 , i ) (o_{i,1}, o_{i,2}, \cdots, o_{i, t-1}, i) ( o i , 1 , o i , 2 , ⋯ , o i , t − 1 , i ) . 这个序列的前 t − 1 t - 1 t − 1 项不会出现 i i i ,即前 t − 1 t - 1 t − 1 项只会从前 i − 1 i - 1 i − 1 个物品中选择,所以 ( o i , 1 , o i , 2 , ⋯ , o i , t − 1 ) ∈ O i − 1 (o_{i,1}, o_{i,2}, \cdots, o_{i, t-1}) \in O_{i - 1} ( o i , 1 , o i , 2 , ⋯ , o i , t − 1 ) ∈ O i − 1 . 由于 S i j S_{ij} S ij 总体积和小于等于 j j j 的要求:
∑ s = 1 t v o i , s ⩽ j \large
\sum_{s = 1}^t v_{o_{i,s}} \leqslant j
s = 1 ∑ t v o i , s ⩽ j
对于任意一个 A 1 A_1 A 1 的元素 o i = ( o i , 1 , o i , 2 , ⋯ , o i , t − 1 , i ) o_i = (o_{i,1}, o_{i,2}, \cdots, o_{i, t-1}, i) o i = ( o i , 1 , o i , 2 , ⋯ , o i , t − 1 , i ) 有:
∑ s = 1 t v o i , s = ∑ s = 1 t − 1 v o i , s + v i ⩽ j \large
\sum_{s = 1}^t v_{o_{i,s}} = \sum_{s = 1}^{t - 1} v_{o_{i,s}} + v_i\leqslant j
s = 1 ∑ t v o i , s = s = 1 ∑ t − 1 v o i , s + v i ⩽ j
故
∑ s = 1 t − 1 v o i , s ⩽ j − v i \large
\sum_{s = 1}^{t - 1} v_{o_{i,s}}\leqslant j - v_i
s = 1 ∑ t − 1 v o i , s ⩽ j − v i
即除去最后一个 i i i 物品,前 t − 1 t - 1 t − 1 个选中的物品的体积和满足小于等于 j − v i j - v_i j − v i . 再加上刚刚得到的 ( o i , 1 , o i , 2 , ⋯ , o i , t − 1 ) ∈ O i − 1 (o_{i,1}, o_{i,2}, \cdots, o_{i, t-1}) \in O_{i - 1} ( o i , 1 , o i , 2 , ⋯ , o i , t − 1 ) ∈ O i − 1 ,我们有 ( o i , 1 , o i , 2 , ⋯ , o i , t − 1 ) ∈ S i − 1 , j − v i (o_{i,1}, o_{i,2}, \cdots, o_{i, t-1}) \in S_{i - 1, j - v_i} ( o i , 1 , o i , 2 , ⋯ , o i , t − 1 ) ∈ S i − 1 , j − v i . 即除去最后一个 i i i 物品的子序列是一个在前 i − 1 i - 1 i − 1 个物品中选,体积和不超过 j − v i j - v_i j − v i 的选法序列。反过来,显然任意一个 S i − 1 , j − v i S_{i-1, j-v_i} S i − 1 , j − v i 中的序列 o i − 1 o_{i-1} o i − 1 ,在末尾拼接上 i i i 之后,就成为 A 1 A_1 A 1 中的某个序列 o i o_i o i ,即 concat ( o i − 1 , ( i ) ) ∈ A 1 \text{concat}(o_{i-1}, (i)) \in A_1 concat ( o i − 1 , ( i )) ∈ A 1 。所以:
g ( A 1 ) = g ( S i − 1 , j − v i ) + w i g(A_1) = g(S_{i - 1, j - v_i}) + w_i
g ( A 1 ) = g ( S i − 1 , j − v i ) + w i
所以
g ( S i j ) = max ( g ( S i − 1 , j − v i + w i ) , g ( S i − 1 , j ) ) g(S_{ij}) = \max(g(S_{i - 1, j - v_i} + w_i), \ g(S_{i - 1, j}))
g ( S ij ) = max ( g ( S i − 1 , j − v i + w i ) , g ( S i − 1 , j ))
即
f ( i , j ) = max ( f ( i − 1 , j − v i ) + w i , f ( i − 1 , j ) ) f(i, j) = \max(f(i - 1, j - v_i) + w_i, \ f(i - 1, j))
f ( i , j ) = max ( f ( i − 1 , j − v i ) + w i , f ( i − 1 , j ))
朴素的实现
注意到只有当 j ⩾ v i j \geqslant v_i j ⩾ v i 时才有 A 1 ≠ ∅ A_1 \neq \emptyset A 1 = ∅ .
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 ( i , j ) 的计算只用到了 f ( i − 1 , ∗ ) f(i - 1, *) f ( i − 1 , ∗ ) ,只需要保存前一轮次的计算结果 f ( i − 1 , ∗ ) f(i - 1, *) f ( i − 1 , ∗ ) . 于是可以省去 f
数组的第一个维度。注意到 f[j - v[i]]
在数组中的位置比 f[j]
靠前,如果正着枚举 j
会导致计算 f[j]
时 f[j - v[i]]
已经被更新,即 f[j - v[i]]
等于 f ( i , j − v i ) f(i, j - v_i) f ( i , j − v i ) 而不是我们需要的 f ( i − 1 , j − v i ) f(i - 1, j - v_i) 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; }