有 n n n 种物品,物品 j j j 的重量和价值分别为 w j w_j w j 和 v j v_j v j ,如果背包的最大容量限制是 m m m ,怎么样选择放入背包的物品以使得背包的总价值最大?
组合优化问题,设 x j x_j x j 表示装入背包的第 j j j 个物品的数量,解可以表示为 < x 1 , x 2 , … , x n > <x_1,x_2,\ldots,x_n> < x 1 , x 2 , … , x n > 。那么目标函数和约束条件是:
如果组合优化问题的目标函数和约束条件都是线性函数,称为线性规划。如果线性规划问题的变量 x j x_j x j 都是非负整数,则称为整数规划问题。背包问题就是整数规划问题。限制所有的 x j = 0 o r x j = 1 x_j = 0 or x_j = 1 x j = 0 or x j = 1 时称为0-1背包。
有n
件物品和容量为m
的背包,给出每件物品的重量 w i w_i w i 以及价值 v i v_i v i ,求解让装入背包的物品重量不超过背包容量,且价值最大 。特点: 每个物品只有一件供你选择放还是不放。
二维解法:设 f [ i ] [ j ] f[i][j] f [ i ] [ j ] 表示前 i i i 件物品 总重量不超过 j j j 的最大价值,可得出状态转移方程:
f [ i ] [ j ] = m a x { f [ i − 1 ] [ j − w [ i ] ] + v [ i ] , f [ i − 1 ] [ j ] } f[i][j]=max\{f[i-1][j-w[i]]+v[i],\ f[i-1][j]\} f [ i ] [ j ] = ma x { f [ i − 1 ] [ j − w [ i ]] + v [ i ] , f [ i − 1 ] [ j ]}
一维解法:设 f [ j ] f[j] f [ j ] 表示重量不超过 j j j 公斤的最大价值,可得出状态转移方程:
f [ j ] = m a x { f [ j ] , f [ j − w [ i ] ] + v [ i ] } f[j]=max\{f[j],\ f[j−w[i]]+v[i]\} f [ j ] = ma x { f [ j ] , f [ j − w [ i ]] + v [ i ]}
二维解法 一维解法
复制 int maxValue (std :: vector < int > & values , std :: vector < int > & weight , int n , int m){
int dp [n + 1 ][m + 1 ];
memset (dp , 0 , sizeof (dp));
for ( int i = 1 ; i <= n; ++ i){
for ( int j = 1 ; j <= m; ++ j){
if ( weight [i] <= j)
dp [i][j] = max ( dp [i - 1 ][j] , dp [i - 1 ][j - weight [i]] + values [i]);
else
dp [i][j] = dp [i - 1 ][j];
}
}
return dp [n][m];
}
复制 int maxValue (std :: vector < int > & values , std :: vector < int > & weight , int n , int m){
int dp [m + 1 ];
memset (dp , 0 , sizeof (dp));
for ( int i = 1 ; i <= n; ++ i){
for ( int j = m; j >= 1 ; -- j){
if ( weight [i] <= j)
dp [j] = max ( dp [j] , dp [j - weight [i]] + values [i]);
}
}
return dp [m];
}
有 n n n 件物品和容量为 m m m 的背包,给出每件物品的重量 w i w_i w i 以及价值 v i v_i v i ,求解让装入背包的物品重量不超过背包容量,且价值最大 。特点: 每个物品可以无限选用 。
一维解法: 设 f [ j ] f[j] f [ j ] 表示重量不超过 j j j 公斤的最大价值,可得出状态转移方程:
f [ j ] = m a x { f [ j ] , f [ j − w [ i ] ] + v [ i ] } f[j]=max\{f[j],\ f[j−w[i]]+v[i]\} f [ j ] = ma x { f [ j ] , f [ j − w [ i ]] + v [ i ]}
复制 int maxValue (std :: vector < int > & values , std :: vector < int > & weight , int n , int m){
int dp [m + 1 ];
memset (dp , 0 , sizeof (dp));
for ( int i = 1 ; i <= n; ++ i){
for ( int j = weight [i]; j <= m; ++ j){
dp [j] = max ( dp [j] , dp [j - weight [i]] + values [i]);
}
}
return dp [m];
}
有 n n n 件物品和容量为 m m m 的背包。给出每件物品的数量 s i s_i s i 、重量 w i w_i w i 以及价值 v i v_i v i ,求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。特点: 每个物品都有了一定的数量 。
二维解法:把 s i s_i s i 个物品逐个拆分,则得到 ∑ s i \sum s_i ∑ s i 个物品,原问题转化为01背包问题,时间复杂度为 O ( m × ∑ s i ) O(m\times \sum s_i) O ( m × ∑ s i ) 。也可以在转移得过程中枚举 k k k ,代表第 i i i 个物品选取的数量,则转移方程为:
f [ i ] [ j ] = m a x 0 ≤ k ≤ s [ i ] { f [ i − 1 ] [ j − k × v [ i ] ] + k × w [ i ] } f[i][j]=\mathop{max}\limits_{0\le k \le s[i]}\{f[i - 1][j - k\times v[i]] + k\times w[i]\} f [ i ] [ j ] = 0 ≤ k ≤ s [ i ] ma x { f [ i − 1 ] [ j − k × v [ i ]] + k × w [ i ]}
一维解法:设 f [ j ] f[j] f [ j ] 表示重量不超过 j j j 公斤的最大价值,可得出状态转移方程:
f [ j ] = m a x 0 ≤ k ≤ s [ i ] { f [ j − k × w [ i ] ] + k × v [ i ] } f[j] = \mathop{max}\limits_{0\le k \le s[i]}\{f[j−k\times w[i]]+k\times v[i]\} f [ j ] = 0 ≤ k ≤ s [ i ] ma x { f [ j − k × w [ i ]] + k × v [ i ]}
复制 int maxValue (std :: vector < int > & values , std :: vector < int > & weight ,
std :: vector < int > & count , int n , int m){
int dp [m + 1 ];
memset (dp , 0 , sizeof (dp));
for ( int i = 1 ; i <= n; ++ i){
for ( int j = m; j >= weight [i]; -- j){
for ( int k = 0 ; k <= count [i]; k ++ ){
if (j - k * weight [i] < 0 )
break ;
dp [j] = max ( dp [j] , dp [j - k * weight [i]] + k * values [i]);
}
}
}
return dp [m];
}
优化:
1、二进制优化
将第 i i i 种物品分成若干件物品,可以有 ( v i , w i ) , ( v i × 2 , w i × 2 ) , ( v i × 4 , w i × 4 ) … (v_i,w_i),(v_i\times 2,w_i\times 2),(v_i\times 4,w_i\times 4)\ldots ( v i , w i ) , ( v i × 2 , w i × 2 ) , ( v i × 4 , w i × 4 ) … 等等,每件物品有一个系数,分别为 1 , 2 , 4 , … , 2 k − 1 , n − 2 k + 1 1,2,4,\ldots,2^{k - 1},n-2^k+1 1 , 2 , 4 , … , 2 k − 1 , n − 2 k + 1 , k k k 是满足的最大整数。例如, n n n 为13,拆分成 1 , 2 , 4 , 6 1,2,4,6 1 , 2 , 4 , 6 四件物品,根据二进制的性质, 0 … 13 0\ldots 13 0 … 13 都可以由这四件物品组合得到。于是, n n n 件物品,就拆分成至多 l o g n logn l o g n 件物品。再转化为01背包问题求解,复杂度降至 O ( m × ∑ l o g n ) O(m\times \sum logn) O ( m × ∑ l o g n ) 。
复制 int maxValue (std :: vector < int > & values , std :: vector < int > & weight ,
std :: vector < int > & count , int n , int m){
vector < pair <int , int> > lis;
lis . push_back ({ 0 , 0 });
int idx , c;
for ( int i = 1 ; i <= n; ++ i){
c = 1 ;
while ( count [i] - c > 0 ){
count [i] -= c;
lis . push_back ({c * values [i] , c * weight [i]});
c *= 2 ;
}
lis . push_back ({ count [i] * values [i] , count [i] * weight [i]});
}
int dp [m + 1 ];
memset (dp , 0 , sizeof (dp));
for ( int i = 1 ; i <= lis . size (); ++ i){
for ( int j = m; j >= lis [i] . second; -- j){
dp [j] = max ( dp [j] , dp [j - lis [i] . second] + lis [i] . first);
}
}
return dp [m];
}
2、单调队列优化
将背包的容量按照的剩余分成组:
仔细观察转移方程,会发现真正的转移只会发生在同一组内,不同的组是不可能转移的可以改一下上面的转移方程:
f [ b + x × v [ i ] ] = m a x 0 ≤ k ≤ s [ i ] { f [ b + ( x − k ) × v [ i ] ] + k × w [ i ] } f[b+x\times v[i]] = \mathop{max}\limits_{0\le k \le s[i]}\{f[b+(x-k)\times v[i]]+k\times w[i]\} f [ b + x × v [ i ]] = 0 ≤ k ≤ s [ i ] ma x { f [ b + ( x − k ) × v [ i ]] + k × w [ i ]}
令 t = x − k t = x - k t = x − k ,状态转移方程变成:
f [ b + x × v [ i ] ] = m a x x − s [ i ] ≤ t ≤ x { f [ b + t × v [ i ] ] − t × w [ i ] } + x × w [ i ] f[b+x\times v[i]] = \mathop{max}\limits_{x-s[i]\le t \le x}\{f[b+t\times v[i]]-t\times w[i]\} + x\times w[i] f [ b + x × v [ i ]] = x − s [ i ] ≤ t ≤ x ma x { f [ b + t × v [ i ]] − t × w [ i ]} + x × w [ i ]
这里的转移实际上一对 ( b , x ) (b,x) ( b , x ) 唯一对应一个 j j j 。我们固定 b b b ,那么对于 x x x 的决策 t t t 的集合正好是一个滑动区间,而 m a x max ma x 里面是一个和 x x x 无关的式子,所以可以用单调队列来维护。具体算法如下:
枚举一个 i i i 和 b ( 0 ≤ b ≤ v [ i ] ) b(0\le b\le v[i]) b ( 0 ≤ b ≤ v [ i ]) ;
从小到大枚举 x x x ,并且用单调队列来维护 f [ b + t × v [ i ] ] − t × w [ i ] f[b+t\times v[i]]-t\times w[i] f [ b + t × v [ i ]] − t × w [ i ] 关于 t t t 单调下降,更新 f [ b + x × v [ i ] ] f[b+x\times v[i]] f [ b + x × v [ i ]] 的值。
由于每个剩余类最多只有 m v [ i ] \frac{m}{v[i]} v [ i ] m 个元素,那么对于一个 i i i ,转移的总时间为 O ( m v [ i ] × v [ i ] ) = O ( m ) O(\frac{m}{v[i]} \times v[i]) = O(m) O ( v [ i ] m × v [ i ]) = O ( m ) ,所以算法的时间复杂度为 O ( n × m ) O(n\times m) O ( n × m ) ,这样我们就又优化掉了多重背包中的物品个数这一维。
复制 int maxValue (std :: vector < int > & values , std :: vector < int > & weight ,
std :: vector < int > & count , int n , int m){
int dp [m + 1 ];
int q [m + 1 ];
int num [m + 1 ];
memset (dp , 0 , sizeof (dp));
int l , r;
for ( int i = 1 ; i <= n; ++ i){
int c = min ( count [i] , m / weight [i]);
for ( int b = 0 ; b < weight [i]; b ++ ){
l = r = 1 ;
for ( int t = 0 ; t <= (m - b) / weight [i]; t ++ ){
int tmp = dp [t * weight [i] + b] - t * values [i];
while (l < r && q [r - 1 ] <= tmp)
r -- ;
q [r] = tmp;
num [r ++ ] = t;
// 滑动区间长度不大于c,因为dp[t * weight[i] + b] - t * values[i]既然存在,
// 那么再加c区间的t * values[i]的值肯定能取到
while (l < r && t - num [l] > c)
l ++ ;
// 因为dp中的是t * weight[i] + b,所以是q[l] + t * values[i]
dp [t * weight [i] + b] = max ( dp [t * weight [i] + b] , q [l] + t * values [i]);
}
}
}
return dp [m];
}