博客园同步
前置知识:
这篇文章我们主要研究 单调队列优化
dp
\text{dp}
dp 如何用于背包问题 和 部分特殊背包问题的优化方式 。
01
\text{01}
01 背包问题
n
n
n 个物品,背包体积为
V
V
V ,每个物品有
v
i
v_i
v i (价值)和
w
i
w_i
w i (重量),每个物品只有
1
1
1 个。求在不超过背包体积的情况下能获得的最大价值。
n
,
V
,
v
i
,
w
i
≤
1
×
1
0
3
n,V,v_i,w_i \leq 1 \times 10^3
n , V , v i , w i ≤ 1 × 1 0 3 ,时间限制
1
s
1s
1 s ,空间限制
16
M
B
16MB
1 6 M B .
简单的考虑,用
f
i
,
j
f_{i,j}
f i , j 表示前
i
i
i 个物品,背包体积为
j
j
j 的答案,则易得:
f
i
,
j
=
max
(
f
i
−
1
,
j
,
f
i
−
1
,
j
−
w
i
+
v
i
)
f_{i,j} = \max(f_{i-1,j} , f_{i-1 , j-w_i} + v_i)
f i , j = max ( f i − 1 , j , f i − 1 , j − w i + v i )
这样可以在
O
(
n
V
)
\mathcal{O}(nV)
O ( n V ) 的时间内解决问题。
但是你发现空间只有
16
MB
16 \text{MB}
1 6 MB ,由于
i
i
i 的决策只取决于上一行同一列,所以可以直接降维,得:
f
j
=
max
(
f
j
,
f
j
−
w
i
+
v
i
)
f_j = \max(f_j , f_{j-w_i} + v_i)
f j = max ( f j , f j − w i + v i )
注意
j
≥
w
i
j \geq w_i
j ≥ w i 的隐约限制。
伪代码如下:
for ( i= 1 ; i<= n; i++ )
for ( j= V; j>= w[ i] ; j-- )
f[ j] = max ( f[ j] , f[ j- w[ i] ] + v[ i] ) ;
O
(
n
V
)
\mathcal{O}(nV)
O ( n V ) 是最优的算法。今天我们所要实现的,仅仅是,把所有的背包算法复杂度都降为
O
(
n
V
)
\mathcal{O}(nV)
O ( n V ) .
倒叙枚举 - 小细节
你可能会在代码里注意到一行:
for ( j= V; j>= w[ i] ; j-- )
为什么是倒序枚举呢?它和
for ( j= w[ i] ; j<= V; j++ )
等价吗?
这是一个初学者常常犯的错误,这两种写法并不等价。可以运用这种写法去解决完全背包。
如果你是正序枚举的话,你会发现,
i
i
i 号物品会被重复使用很多次。比方说
w
i
=
2
,
v
i
=
3
w_i = 2 , v_i = 3
w i = 2 , v i = 3 时,那么
j
=
4
j=4
j = 4 就会从
j
=
2
j=2
j = 2 转移过来,此时 程序已经把
i
i
i 物品用了
2
2
2 次,这样是不可取的 。而反之在完全背包中则是可取的,因为完全背包问题是 无限个数 ,可以这样写。
当然如果你倒序枚举的话,此时对于所有
j
=
w
i
,
j
=
2
×
w
i
⋯
⋯
j = w_i , j = 2 \times w_i \cdots \cdots
j = w i , j = 2 × w i ⋯ ⋯ 中,就只会更新
j
=
w
i
j=w_i
j = w i 的,因为倒序的时候
j
j
j 从
j
−
w
i
j-w_i
j − w i 来,前一个还没有推出来就推导后一个,肯定没有答案——这样正好是我们所求的。而
j
=
w
i
j=w_i
j = w i 有答案的原因就是因为本身的一次更新。
之后遇到的各种
dp
\text{dp}
dp 都会出现类似此类的问题,大家一定要小心!
完全背包问题
n
n
n 个物品,背包体积为
V
V
V ,每个物品有
v
i
v_i
v i (价值)和
w
i
w_i
w i (重量),每个物品有无限个。求在不超过背包体积的情况下能获得的最大价值。
n
,
V
,
v
i
,
w
i
≤
1
×
1
0
3
n,V,v_i,w_i \leq 1 \times 10^3
n , V , v i , w i ≤ 1 × 1 0 3 ,时间限制
1
s
1s
1 s ,空间限制
16
M
B
16MB
1 6 M B .
这样你会发现一个问题,你不知道有多少个!
当然你可以用
⌊
V
v
i
⌋
\lfloor \frac{V}{v_i} \rfloor
⌊ v i V ⌋ 来表示数量,考虑枚举。
同样的
f
i
,
j
f_{i,j}
f i , j ,我们可以知道:
f
i
,
j
=
max
k
=
0
⌊
V
v
i
⌋
(
f
i
−
1
,
j
−
k
×
w
i
+
k
×
v
i
)
f_{i,j} = \max_{k=0}^{\lfloor \frac{V}{v_i} \rfloor}(f_{i-1,j-k \times w_i} + k \times v_i)
f i , j = k = 0 max ⌊ v i V ⌋ ( f i − 1 , j − k × w i + k × v i )
大前提是
j
≥
k
×
w
i
j \geq k \times w_i
j ≥ k × w i ,这样转移的复杂度是
O
(
n
V
w
i
)
\mathcal{O}(nVw_i)
O ( n V w i ) ,降维之后应该可以通过。
考虑一个简单的加强:
n
,
V
,
v
i
,
w
i
≤
1
×
1
0
4
n,V,v_i,w_i \leq 1 \times 10^4
n , V , v i , w i ≤ 1 × 1 0 4 ,空间限制
128
M
B
128MB
1 2 8 M B .
我们将在下面的研究中,解决这个问题。
多重背包问题
n
n
n 个物品,背包体积为
V
V
V ,每个物品有
v
i
v_i
v i (价值)和
w
i
w_i
w i (重量),每个物品有
num
i
\text{num}_i
num i 个。求在不超过背包体积的情况下能获得的最大价值。
n
,
V
,
v
i
,
w
i
≤
1
×
1
0
4
n,V,v_i,w_i \leq 1 \times 10^4
n , V , v i , w i ≤ 1 × 1 0 4 ,时间限制
1
s
1s
1 s ,空间限制
128
M
B
128MB
1 2 8 M B .
显然我们按照完全背包的方法就可以了,但是
O
(
n
V
w
i
)
\mathcal{O}(nVw_i)
O ( n V w i ) 是不可能通过
1
0
4
10^4
1 0 4 的!所以我们需要考虑,如何优化这个问题。
实际上不用单调队列也可以优化,我们先来讲著名的背包优化的几个方法。
二进制拆分
二进制!这是个熟悉的东西。
实际上我们背包的瓶颈在于两个:背包物品过多,背包容积过大,背包物品数量过多。
容积过大可能会想到离散,但是这不是排序之类只用知道大小的问题啊!容积是不能突破的,考虑优化物品个数。物品个数怎么可能优化呢?数量?
那你可能会说,这和二进制又有什么关系?
下面我们来说一说吧。
用二进制表示数
用集合
s
s
s 表示二的幂次集,则
s
=
{
2
0
,
2
1
⋯
2
∞
}
s = \{ 2^0 , 2^1 \cdots 2^{\infty}\}
s = { 2 0 , 2 1 ⋯ 2 ∞ } .每个正整数都可以用若干个
s
s
s 集合内的元素相加而成。就是说每个数一定能被分解成若干二的幂次的数之和。
10
=
2
+
8
10 = 2 +8
1 0 = 2 + 8
100
=
64
+
32
+
4
100 = 64 + 32 +4
1 0 0 = 6 4 + 3 2 + 4
1000
=
512
+
256
+
128
+
64
+
32
+
8
1000 = 512 + 256 + 128 + 64 + 32 + 8
1 0 0 0 = 5 1 2 + 2 5 6 + 1 2 8 + 6 4 + 3 2 + 8
如何证明?
显而易见,每个数都能被二进制表示。比方说
1
0
10
=
100
1
2
10_{10} = 1001_2
1 0 1 0 = 1 0 0 1 2 .
那么,用计数原则,
100
1
2
=
1
×
2
0
+
0
×
2
1
+
0
×
2
2
+
1
×
2
3
1001_2 = 1 \times 2^0 + 0 \times 2^1 + 0 \times 2^2 + 1 \times 2^3
1 0 0 1 2 = 1 × 2 0 + 0 × 2 1 + 0 × 2 2 + 1 × 2 3
这样就证明结束了,你没有发现吗?
拆分原理
将一个
{
v
i
,
w
i
,
n
u
m
i
}
\{ v_i , w_i , num_i\}
{ v i , w i , n u m i } 的物品,通过
n
u
m
i
num_i
n u m i 进行拆分。
本来假设一个物品有
7
7
7 个,你可以把它拆开成
1
,
2
,
4
1,2,4
1 , 2 , 4 个,对这三个物品进行
01
01
0 1 背包,你就可以得到
1
−
7
1 - 7
1 − 7 所有可能的答案。最后你把不取的答案单独做一遍就可以了。
原理就是,
n
n
n 最多被拆成
log
n
\log n
log n 个二的幂次的数之和,这样保证了时间复杂度是
O
(
n
V
log
w
i
)
\mathcal{O}(nV \log w_i)
O ( n V log w i ) 的。
伪代码
for ( i= 1 ; i<= n; i++ ) {
read ( x) , read ( y) , read ( z) ;
for ( j= 1 ; j<= z; j<<= 1 ) v[ ++ cnt] = x* j, w[ cnt] = y* j, z- = j;
if ( z) v[ ++ cnt] = x* z, w[ cnt] = y* z;
}
单调队列
显然,
1
0
4
10^4
1 0 4 的数据可以把二进制拆分卡死。我们需要严格去掉
log
\log
log .
再看一眼这个状态转移:
f
i
,
j
=
max
k
=
0
num
i
(
f
i
−
1
,
j
−
k
×
w
i
+
k
×
v
i
)
f_{i,j} = \max_{k=0}^{\text{num}_i}(f_{i-1,j-k \times w_i} + k \times v_i)
f i , j = k = 0 max num i ( f i − 1 , j − k × w i + k × v i )
你会发现,这不是一段连续的决策!这是断续的。
但是,你会发现这样一个问题。
f
i
−
1
,
j
,
f
i
−
1
,
j
−
w
i
,
f
i
−
1
,
j
−
2
×
w
i
⋯
f
i
−
1
,
j
−
num
i
×
w
i
f_{i-1 , j} , f_{i-1 , j - w_i} , f_{i-1 , j - 2 \times w_i} \cdots f_{i-1 , j - \text{num}_i \times w_i}
f i − 1 , j , f i − 1 , j − w i , f i − 1 , j − 2 × w i ⋯ f i − 1 , j − num i × w i ,这是所有
f
i
,
j
f_{i,j}
f i , j 的决策点。
你会发现,这些决策点是同行等差列的,每两个决策点隔着一个
w
i
w_i
w i . 这非常好!
余数构造连续决策
你会发现,你可以把
V
V
V 按照模
w
i
w_i
w i 进行分类,余数相同的肯定是一类的。
那样我们就可以把这一段当成连续的决策去做,因为实际上枚举余数
mo
\text{mo}
mo 和当前周期编号
k
k
k . 因为
k
k
k 是连续的,那么
mo
+
k
×
w
i
\text{mo} + k \times w_i
mo + k × w i 实际上就是当前的决策点。这样我们构造出了若干段连续的决策。
时间复杂度分析
对
n
n
n 个物品每次做一次,所以
O
(
n
)
\mathcal{O}(n)
O ( n ) .
然后同时枚举余数和
k
k
k ,因为
mo
×
k
≤
V
\text{mo} \times k \leq V
mo × k ≤ V ,所以本质上这两重循环的
∑
\sum
∑ 不会超过
V
V
V ,是
O
(
V
)
\mathcal{O}(V)
O ( V ) .
按照单调队列每个节点进出一次的原理,
O
(
n
V
)
\mathcal{O}(nV)
O ( n V ) 的目标已然达成。
for ( i= 1 ; i<= n; i++ ) {
read ( v) , read ( w) , read ( num) ;
num= min ( num, V/ v) ;
for ( mo= 0 ; mo< v; mo++ ) {
l= r= 0 ;
for ( k= 0 ; k<= ( V- mo) / v; k++ ) {
x= k, y= f[ k* v+ mo] - k* w;
while ( l< r && q[ l] . pos< k- num) l++ ;
while ( l< r && q[ r- 1 ] . val<= y) r-- ;
q[ r] . val= y, q[ r++ ] . pos= x;
f[ k* v+ mo] = q[ l] . val+ k* w;
}
}
}
完全背包问题 - 单调队列
这里我们发现,直接把
⌊
V
v
i
⌋
\lfloor \frac{V}{v_i} \rfloor
⌊ v i V ⌋ 作为
num
\text{num}
num 就可以直接通过。这样我们实现了完全背包和多重背包的
O
(
n
V
)
\mathcal{O}(nV)
O ( n V ) .
下面,有了多重背包和完全背包的基础,我们将来解决更套路性的问题。
混合背包问题
n
n
n 个物品,背包体积为
V
V
V ,每个物品有
v
i
v_i
v i (价值)和
w
i
w_i
w i (重量),每个物品的个数用符号
t
t
t 表示。
t
=
−
1
t=-1
t = − 1 表示该物品只有
1
1
1 个,
t
=
0
t=0
t = 0 表示该物品有无限个,
t
>
0
t>0
t > 0 表示该物品有
t
t
t 个。求在不超过背包体积的情况下能获得的最大价值。
n
,
V
,
v
i
,
w
i
≤
1
×
1
0
4
n,V,v_i,w_i \leq 1 \times 10^4
n , V , v i , w i ≤ 1 × 1 0 4 ,时间限制
1
s
1s
1 s ,空间限制
128
M
B
128MB
1 2 8 M B .
很显然,我们可以把
01
01
0 1 背包的物品看成是
num
i
=
1
\text{num}_i = 1
num i = 1 的多重背包物品 ,完全背包的物品看成是
num
i
=
⌊
V
v
i
⌋
\text{num}_i = \lfloor \frac{V}{v_i} \rfloor
num i = ⌊ v i V ⌋ 的多重背包物品 ,最后用
O
(
n
V
)
\mathcal{O}(nV)
O ( n V ) 跑一遍多重背包即可。
代码略。
二维费用背包问题
n
n
n 个物品,背包体积为
V
V
V ,所能承受的最大重量为
W
W
W .每个物品有
v
i
v_i
v i (价值)和
w
i
w_i
w i (重量)和
g
i
g_i
g i (体积)。求在不超过背包体积 且 不超过最大载重的情况下能获得的最大价值。(每个物品只有
1
1
1 个)
n
,
V
,
v
i
,
w
i
≤
5
×
1
0
2
n,V,v_i,w_i \leq 5 \times 10^2
n , V , v i , w i ≤ 5 × 1 0 2 ,时间限制
1
s
1s
1 s ,空间限制
128
M
B
128MB
1 2 8 M B .
显然,我们出现了另一层限制,使得我们无法用
O
(
n
V
)
\mathcal{O}(nV)
O ( n V ) 的时间解决。实际上时间限制也暗示了这一点。我们应该从头开始,重新推导。其实大多过程是类似的。
基础推导过程
用
f
i
,
j
,
k
f_{i,j,k}
f i , j , k 表示前
i
i
i 个物品,体积为
j
j
j ,载重为
k
k
k 的最大价值。
那么可得:
f
i
,
j
,
k
=
max
(
f
i
−
1
,
j
,
k
,
f
i
−
1
,
j
−
g
i
,
k
−
w
i
+
v
i
)
f_{i,j,k} = \max(f_{i-1,j,k} , f_{i-1,j-g_i , k - w_i} + v_i)
f i , j , k = max ( f i − 1 , j , k , f i − 1 , j − g i , k − w i + v i )
按照这个方式,
O
(
n
V
W
)
\mathcal{O}(nVW)
O ( n V W ) 足以解决
5
×
1
0
2
5 \times 10^2
5 × 1 0 2 的数据。
伪代码如下:
for ( i= 1 ; i<= n; i++ ) {
read ( g) , read ( w) , read ( v) ;
for ( j= V; j>= g; j-- )
for ( k= W; k>= w; k-- )
f[ j] [ k] = max ( f[ j] [ k] , f[ j- g] [ k- w] + v) ;
}
拓展二维背包问题
假设,每个物品有
num
i
\text{num}_i
num i 个,其余按照二维背包条件不变,
5
×
1
0
2
5 \times 10^2
5 × 1 0 2 仍不变,如何追求一个
O
(
n
V
W
)
\mathcal{O}(nVW)
O ( n V W ) 的算法?
实际上单调队列仅仅是让我们 完全地把背包的复杂度寄托于物品的个数与限制条件,而与具体数值大小无关 ,所以单调队列是可以做到的。
f
i
,
j
,
k
=
max
l
=
0
num
i
(
f
i
−
1
,
j
−
l
×
w
i
,
k
−
l
×
g
i
+
l
×
v
i
)
f_{i,j,k} = \max_{l=0}^{\text{num}_i} (f_{i-1,j-l \times w_i , k-l \times g_i} + l \times v_i)
f i , j , k = l = 0 max num i ( f i − 1 , j − l × w i , k − l × g i + l × v i )
但是,这个你和一维的背包问题比较一下呢:
f
i
,
j
=
max
k
=
0
num
i
(
f
i
−
1
,
j
−
k
×
w
i
+
k
×
v
i
)
f_{i,j} = \max_{k=0}^{\text{num}_i}(f_{i-1,j-k \times w_i} + k \times v_i)
f i , j = k = 0 max num i ( f i − 1 , j − k × w i + k × v i )
一维背包的周期是
w
i
w_i
w i .按照这个说法,二维背包的周期应该是
lcm
(
w
i
,
g
i
)
\operatorname{lcm}(w_i , g_i)
l c m ( w i , g i ) ,可以认为是
w
i
×
g
i
w_i \times g_i
w i × g i .
我们需要同时枚举两个模数
mo1
\text{mo1}
mo1 和
mo2
\text{mo2}
mo2 ,用
mo1
×
w
i
+
mo2
\text{mo1} \times w_i + \text{mo2}
mo1 × w i + mo2 来表示当前节点,然后用单调队列做即可。
时间复杂度仍然是
O
(
n
V
W
)
\mathcal{O}(nVW)
O ( n V W ) ,一个道理,每个节点入队出队一次。
但是严格意义上这个做法是不对的。为什么呢?
拓展多维背包问题
在拓展二维的基础上,把每个物品的限制加到
k
k
k 个,能否做到
O
(
n
V
W
)
\mathcal{O}(nVW)
O ( n V W ) ?答案是不能。
O
(
n
V
W
k
)
\mathcal{O}(nVWk)
O ( n V W k ) ?这当然可以。
这样对于
k
k
k 个限制的相乘,单调队列的优化已经基本无用。因为在完全随机的,
k
≥
3
k \geq 3
k ≥ 3 的情况之下,
k
k
k 个数的乘积很容易就超过了
max
i
=
1
n
w
i
\max_{i=1}^n w_i
max i = 1 n w i 使得优化失败,无法进行单调队列操作。
所以,
2
2
2 维也是一个道理,两个随机的
≤
5
×
1
0
2
\leq 5 \times 10^2
≤ 5 × 1 0 2 的数相乘超过
5
×
1
0
2
5 \times 10^2
5 × 1 0 2 的概率很大。一旦出现这样的物品,我们的单调队列等同于大暴力。而大暴力的复杂度恰恰是
O
(
n
V
W
k
)
\mathcal{O}(nVWk)
O ( n V W k ) ,对于
k
k
k 个限制还需要进行循环判断,常数较大。
所以,一般的题目会给定
k
k
k 的值(并且一般来说
k
≤
3
k \leq 3
k ≤ 3 ),把
k
k
k 当做常数来看!
分组背包问题
n
n
n 个物品,背包体积为
V
V
V .每个物品有
v
i
v_i
v i (价值)和
w
i
w_i
w i (重量),只有
1
1
1 个。 每个物品属于第
s
i
s_i
s i 组。求不超过背包体积 且 每组只选一个物品的最大价值。
n
,
V
,
v
i
,
w
i
≤
1
×
1
0
3
n,V,v_i,w_i \leq 1 \times 10^3
n , V , v i , w i ≤ 1 × 1 0 3 ,时间限制
1
s
1s
1 s ,空间限制
128
M
B
128MB
1 2 8 M B .
很明显,我们应该考虑
dp
\text{dp}
dp 的推导。用
f
k
,
j
f_{k,j}
f k , j 表示前
k
k
k 组物品重量为
j
j
j 所得的最大价值,易得:
f
k
,
j
=
max
(
f
k
−
1
,
j
,
max
s
i
=
k
f
k
−
1
,
j
−
w
i
+
v
i
)
f_{k,j} = \max(f_{k-1,j} , \max_{s_i=k} f_{k-1,j-w_i} + v_i)
f k , j = max ( f k − 1 , j , s i = k max f k − 1 , j − w i + v i )
显然我们已经得到了一个
O
(
n
2
V
)
\mathcal{O}(n^2V)
O ( n 2 V ) 的算法,因为最大组数和
n
n
n 是同阶的,就算成
n
n
n 吧。
这里是没有办法优化的,因为
w
i
w_i
w i 不连续,单调队列不行。
伪代码如下:
for ( i= 1 ; i<= n; i++ ) {
read ( s) ;
for ( j= 1 ; j<= s; j++ ) read ( w[ j] ) , read ( v[ j] ) ;
for ( j= V; j>= 0 ; j-- )
for ( k= 1 ; k<= s; k++ )
if ( j>= w[ k] ) f[ j] = max ( f[ j] , f[ j- w[ k] ] + v[ k] ) ;
}
有依赖的背包问题
这一道题目将是背包问题中最难的一种。
n
n
n 个物品,背包体积为
V
V
V .每个物品有
v
i
v_i
v i (价值)和
w
i
w_i
w i (重量),只有
1
1
1 个。 每个物品有一个依赖物品
p
i
p_i
p i ,如果选了
i
i
i 物品,则必须选
p
i
p_i
p i 号物品。求不超过背包体积的最大价值。数据保证 每个物品最多依赖于一个物品 。(注:因此此类题常常用树形结构给出,儿子节点和父节点呈依赖关系)
n
,
V
,
v
i
,
w
i
≤
1
×
1
0
3
n,V,v_i,w_i \leq 1 \times 10^3
n , V , v i , w i ≤ 1 × 1 0 3 ,时间限制
1
s
1s
1 s ,空间限制
128
M
B
128MB
1 2 8 M B .
显然,这种有依赖的背包并不容易,这和树形
dp
\text{dp}
dp 有些类似。简单的,如果选了
i
i
i ,所有
i
i
i 的祖先都会被选。
一个非常显然的思路是,直接把所有子树的答案计算,合并即可。对于每一个子树,先不考虑根节点,进行背包,然后最后加上根节点的值即可。(因为根节点无论如何会被选,除非全不选)
代码略。
泛化物品
背包中比较玄乎的东西,选看选做。
泛化物品的精髓就是,每个物品没有固定的价值和重量,其价值随着分配的重量而变化。简单的,你用
x
x
x 的重量去放它,它就会产生
h
x
h_x
h x 的价值贡献。而
h
x
h_x
h x 的具体计算会给你一个多项式。这是非常好的一类问题。这一类问题的解决相当繁琐。
比方说吧,若背包容量为
10
10
1 0 ,
h
1
=
3
x
+
2
h_1 = 3x + 2
h 1 = 3 x + 2 ,一个物品
h
2
=
2
x
+
3
h_2 = 2x+3
h 2 = 2 x + 3 ,显然你直接放
1
1
1 个
h
1
h1
h 1 ,价值就可以达到
32
32
3 2 。这意思不是说贪心可以解决背包,而是这类问题不好解决。
简单的透露一下,该问题的复杂度应当是
O
(
V
2
)
\mathcal{O}(V^2)
O ( V 2 ) ,具体内容可以参考文末的 泛化物品_百度百科 .
求方案数 & 具体方案
非常简单,只需要再开一个,算出
f
f
f 后,用 与其同样维数的
g
g
g 记录当前最优解的方案数 。只需要把所有的决策点的
g
g
g 统计一遍即可得到新的
g
g
g .
求具体方案也一样,一般来说会让你求字典序最小的,那么你用 与
f
f
f 同样维数的
g
g
g 记录当前最优解且字典序最小的 那一个编号,这样就可以解决问题。
如果让你求全部的最优方案,你需要开一个
vector
\text{vector}
vector 和
f
f
f 维数一样,类似于 vector<int> h[N][M]
的操作,开二维数量个
vector
\text{vector}
vector ,记录所有答案,最后迭代返回。
由于方案数的增加呈指数性,一般只会求特殊的具体方案 / 方案数取模。
附录:背包问题的搜索解法
以最简单的
01
01
0 1 背包为例,如何用朴素搜索解决?
大力搜索
枚举每个物品选或不选,这样复杂度是
O
(
2
n
)
\mathcal{O}(2^n)
O ( 2 n ) 的,可以加上剪枝(最主要的:就是当前答案加上所有未选的价值之和也不如之前得到的答案,或者是重量超出限制),但也只能 勉强 通过
n
=
25
n=25
n = 2 5 的数据。
一个基于贪心的优化,将重量小 / 价值大 / 性价比高的物品先搜索,会适当提高效率。但是哪怕剪枝到极限,最多也是
n
=
26
,
27
n=26,27
n = 2 6 , 2 7 的样子。
NP
\text{NP}
NP 完全问题
考虑这是一个
NP
\text{NP}
NP 完全,所以从模板题出发:
给定集合
S
S
S ,求是否存在集合
X
X
X 使得
X
X
X 中的元素之和为
k
k
k .
∣
S
∣
≤
42
|S| \leq 42
∣ S ∣ ≤ 4 2 .
显然大力枚举
2
42
2^{42}
2 4 2 是不可能通过的。我们考虑一个简单的优化。
将
S
S
S 等分 为两部分
S
1
S1
S 1 和
S
2
S2
S 2 ,然后枚举
S
1
S1
S 1 和
S
2
S2
S 2 内所有的可能,并两两相加验证。这样复杂度是
O
(
2
⌊
n
2
⌋
)
\mathcal{O}(2^{\lfloor \frac{n}{2} \rfloor})
O ( 2 ⌊ 2 n ⌋ ) 的,可以通过
n
=
42
n=42
n = 4 2 的数据。
当然作者可以自行尝试分更多份,实际上大概率效率不会更高。
折半搜索
实际上上面解决
NP
\text{NP}
NP 完全的方法就是折半搜索的精髓了,对两边分别大力搜索即可实现
O
(
2
⌊
n
2
⌋
)
\mathcal{O}(2^{\lfloor \frac{n}{2} \rfloor})
O ( 2 ⌊ 2 n ⌋ ) 的复杂度。
搜索
VS DP
\text{VS DP}
VS DP
一般来说,设计到多重,完全,肯定是
dp
\text{dp}
dp .只有
01
01
0 1 背包需要进行讨论!
如果
w
i
,
V
≤
1
0
16
,
n
≤
40
w_i,V \leq 10^{16} , n \leq 40
w i , V ≤ 1 0 1 6 , n ≤ 4 0 ,那明显是折半搜索。
如果
w
i
,
V
,
n
≤
5
×
1
0
3
w_i,V,n \leq 5 \times 10^3
w i , V , n ≤ 5 × 1 0 3 ,那明显是
dp
\text{dp}
dp .
总之,面向数据编程是没有毛病的,具体情况具体分析吧。
课后习题
(
Acwing
\text{Acwing}
Acwing 上面
9
9
9 道题,
洛谷
\text{洛谷}
洛谷 上
1
1
1 道,一共
10
10
1 0 道不算多吧)
01
\text{01}
01 背包
完全背包
多重背包 - 单调队列
多重背包
1
1
1 - 大暴力
多重背包
2
2
2 - 二进制拆分
多重背包
3
3
3 - 单调队列
混合背包
二维费用背包
分组背包
有依赖的背包
参考资料
泛化物品_百度百科