DP 背包类型
在开始多重背包以前,先要了解各种背包的类型.
总体抽象模型概述:
有一"容量
V
V
V",现有
a
1
,
a
2
⋯
a
n
{a_1,a_2\cdots a_n}
a1,a2⋯an .有以下三种信息:
"体积 " :
w
1
,
w
2
⋯
w
n
{w_1,w_2\cdots w_n}
w1,w2⋯wn
"价值 " :
v
1
,
v
2
⋯
v
n
{v_1,v_2\cdots v_n}
v1,v2⋯vn
元素数量数 :
c
1
,
c
2
⋯
c
n
{c_1,c_2\cdots c_n}
c1,c2⋯cn
通俗来讲,就是一个容量为
V
V
V的背包,有
n
n
n种物品,各有其价值,体积,数量.
要求:
- ∑ i = 1 n w i ⩽ V \sum_{i=1}^nw_i\leqslant V ∑i=1nwi⩽V
- m a x ∑ i = 1 n k i v i ( 0 ⩽ k i ⩽ c i ) max{\sum_{i=1}^nk_iv_i(0\leqslant k_i \leqslant c_i)} max∑i=1nkivi(0⩽ki⩽ci)
下文是背包的各种具体形式:
-
0
−
1
0-1
0−1背包:
c
=
1
c=1
c=1.
即所有物品只有一个(选/不选). - 完全背包:
c
=
∞
c= \infty
c=∞.
即所有物品有无限个. - 多重背包:
c
i
=
k
i
c_i=k_i
ci=ki.
即所有物品有 x x x个( 0 ⩽ x ⩽ k 0\leqslant x\leqslant k 0⩽x⩽k)
还有分组背包,二维(多维)背包等多种变种背包.
其中多重背包问题是最难的常见背包.
朴素算法
在之前已经介绍过 0 − 1 0-1 0−1背包的计算法:
int dp[N][M];
int main()
{
for (int i=1;i<=n;i++)
for (int j=0;j<=m;j++)
if (j>=w[i]) dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
else dp[i][j]=dp[i-1][j];
printf("%d\n",dp[n][m]);
}
所以不难以想到多重背包的计算法则:
int dp[N][M];
int main()
{
for (int i=1;i<=n;i++)
for (int k=1;k<=c[i];k++)
for (int j=0;j<=m;j++)
if (j>=w[i]) dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
else dp[i][j]=dp[i-1][j];
printf("%d\n",dp[n][m]);
}
这样的复杂度是
O
(
n
m
K
)
O(nmK)
O(nmK),这是不能被接受的.
所以,我们需要优化.
优化
这里主要介绍两种优化.
二进制优化
所谓二进制优化,就是利用了二进制的性质.
其实利用了数据的无序性.
也就是说,使用第一、三、四个物品和第二、八、二十四个物品没有区别.
所以可以比较自然地想到用二进制位的代表性数字.
类似于压位,只要把所有二进制位变成一个物品.
这样,第一个物品代表1个物品,第二个物品代表2个物品…
复杂度降到了
O
(
n
m
l
o
g
K
)
O(nmlogK)
O(nmlogK),并且常数很小,满足我们的需求了(?).
for (int i=1;i<=n;i++)
{
int v=a[i].cnt,tmp=0;
for (int j=1;j<=v;v-=j,j*=2)
x[++tmp]=j*a[i].w,y[tmp]=j*a[i].val;//用新"物品"代替旧物品
if (v>0) x[++tmp]=v*a[i].w,y[tmp]=v*a[i].val;
for (int j=1;j<=tmp;j++)
for (int k=V;k>=x[j];k--)
dp[k]=max(dp[k],dp[k-x[j]]+y[j]);//按原方法进行BF算法
}
能做到这个程度,已经很不容易.
但目前最优的单调队列算法,已经能将这个多重背包优化到
O
(
n
m
)
O(nm)
O(nm)的复杂度.
单调队列算法
单调队列算法,进一步利用的数据的无序性.
我们先作一个铺垫.
现有数据,其体积,价值都相等.
如果原状态为
d
p
[
i
−
1
]
[
p
]
dp[i-1][p]
dp[i−1][p].
那么由这个状态所推出的"最佳状态"只能在
d
p
[
i
]
[
p
+
k
∗
w
i
]
+
k
∗
v
i
dp[i][p+k*w_i]+k*v_i
dp[i][p+k∗wi]+k∗vi中拓展.
换句话说,这些状态扩展的第二维都属于一个同余类.
所以我们可以换用这样的遍历方式:
for (int j=0;j<a[i].w;j++)
for (int k=0;j+k*a[i].w<=V;k++)
这样也将
1..
n
1..n
1..n遍历了一遍,只不过更具有特殊性.
下面我们可以进入真正的精华部分——单调队列.
考虑刚才的
d
p
[
i
]
[
p
+
k
∗
w
i
]
+
k
∗
v
i
dp[i][p+k*w_i]+k*v_i
dp[i][p+k∗wi]+k∗vi.
可以表示为
d
p
[
i
−
1
]
[
p
+
(
k
−
1
)
∗
w
i
+
w
i
]
+
(
k
−
1
)
∗
v
i
+
v
i
dp[i-1][p+(k-1)*w_i+w_i]+(k-1)*v_i+v_i
dp[i−1][p+(k−1)∗wi+wi]+(k−1)∗vi+vi
拓展一波之后可得:
d
p
[
i
]
[
p
+
u
∗
w
i
]
=
d
p
[
i
−
1
]
[
p
+
k
∗
w
i
]
−
k
∗
v
i
+
u
∗
v
i
(
u
−
c
n
t
i
⩽
k
⩽
u
−
1
)
dp[i][p+u*w_i]=dp[i-1][p+k*w_i]-k*v_i+u*v_i\\(u-cnt_i \leqslant k \leqslant u-1)
dp[i][p+u∗wi]=dp[i−1][p+k∗wi]−k∗vi+u∗vi(u−cnti⩽k⩽u−1)
这里就很玄妙了嘿嘿嘿.
回顾单调队列的问题,有比较典型的滑动窗口问题.
在这里,可以视前面的
d
p
[
i
−
1
]
[
p
+
k
∗
w
i
]
−
k
∗
v
i
dp[i-1][p+k*w_i]-k*v_i
dp[i−1][p+k∗wi]−k∗vi为定值.(在此设为
x
k
x_k
xk)
则将所有
x
k
x_k
xk增序推入队列,按照滑动窗口做就行了.
优秀!
(独秀你坐下)
总体复杂度,因为只遍历所有元素一遍,所以是 O ( n m ) O(nm) O(nm)
for (int i=1;i<=n;i++)
{//q 为元素名
for (int j=0;j<a[i].w;j++)
{
int L=0,R=-1;
for (int k=0;j+k*a[i].w<=V;k++)
{
int p=j+k*a[i].w;
q tmp;
tmp.cnt=k,tmp.val=dp[j+k*a[i].w]-k*a[i].val;
while (L<=R&&tmp.val>Q[R].val) R--;
while (L<=R&&k-Q[L].cnt>a[i].cnt) L++;
Q[++R]=tmp;//套用单调队列
dp[p]=max(Q[L].val+k*a[i].val,dp[p]);
}
}
}
这个算法理论上比二进制优秀,但是由于…各种常数原因,加上二进制代码易懂易写,所以还是因地制宜,选择恰当的才好.
后记
由于这几天较忙,所以写得慢.
希望各位奆老谅解! qwq ?