-
背包dp
背包问题初始化的优化:
若不要求装满背包,则初始化为0,因为值为0就是没有任何物品或没有空间时的合法状态。若要求装满背包,则初始化为0-∞,因为-∞就是当前容量还没有任何物品放入,不是合法状态,需要后面装满才行。
装不装满背包只是初始化的不同,其他相同。
-
0/1背包
每个物品最多只能放一次- 0/1背包要点
- 最优子结构:
我们把这种子问题最优时母问题通过优化选择后一定最优的情况叫做“最优子结构”。 - 子问题重叠:
在子问题中,其实都是同样的问题:即有一定容量的背包,一定价值,体积的物品,要在背包容量范围内使拿到的价值总和最大。只是在不同子问题中的数据参数不同。 - 边界:
当子问题不可再分解时,就是该问题的边界。即无法再把物品放入背包,或背包容量满或者物品全部拿完。 - 子问题独立
母问题选择时,最终只会采用一种方案,而各自子问题之间如何选择互不影响。 - 状态转移方程
整个答案最关键的地方
点我看大佬博客
经典金矿dp代码
#include<bits/stdc++.h>
using namespace std;
//f(G[i],w)=max(f(G[i-1],w)+f(G[i-1],w-P[i])+G[i])
int P[5]= {77,22,29,50,90},G[5]= {92,22,87,46,90};
int maxn[101][5]={-1};
int fun(int w,int i)
{
int maxm=0;
if(maxn[w][i]!=-1)
return maxn[w][i];
else if(i==0)
{
if(w>=P[0])
{
maxm=G[0];
}
else maxm=0;
}
else
{
if(w>=P[i])
{
maxm=max(fun(w,i-1),fun(w-P[i],i-1)+G[i]);
}
else
{
maxm=fun(w,i-1);
}
}
maxn[w][i]=maxm;
return maxm;
}
int main()
{
for(int i=0; i<=100; i++)
for(int j=0; j<5; j++)
maxn[i][j]=-1;
cout<<fun(100,4);
for(int i=0; i<100; i++)
{
for(int j=0; j<5; j++)
cout<<maxn[i][j]<<" ";
cout<<endl;
}
return 0;
}
特别注意:
初始化maxn数组的for循环里,i=100,而不是i<100
因为i是工人数目,虽然工人有100个,但是第零个在程序中没有意义,没有计算在内。
0/1背包即:
有 N 件物品和一个容量为 V 的背包。第 i 件物品的体积是 c[i],价值是 w[i]。求解将哪些物品装入背包可使价值总和最大。
用非递归形式和一维数组存储可以再优化?
非递归:
d
p
[
i
]
[
v
]
=
m
a
x
(
d
p
[
i
−
1
]
[
v
]
,
d
p
[
i
−
1
]
[
v
−
c
[
i
]
]
+
w
[
i
]
)
dp[i][v]=max(dp[i-1][v],dp[i-1][v-c[i]]+w[i])
dp[i][v]=max(dp[i−1][v],dp[i−1][v−c[i]]+w[i])
for(int i=0;i<=V;i++)
dp[0][i]=dp[i][0]=0;//没有第0个物品,其价值为0。
//总空间为0时,第i个物品
for(int i=1;i<=N;i++)//第1个物品,有0……V个总空间,分别求价值最大
for(int v=0;v<=V;v++)
if(c[i]<=v)
dp[i][v]=max(dp[i-1][v],dp[i-1][v-c[i]]+w[i]);
else dp[i][v]=dp[i-1][v];
时间复杂度和空间复杂度均为 O ( V ∗ N ) O(V*N) O(V∗N)
优化空间复杂度
按照递归式可知:①dp[i][v]只和第i-1行的值有关。
②而且,只和v到V列的值有关。因为小于v列的话,容量根本放不下第i个物品。
这样,我们从V到v反向更新一维数组即可。
memset(dp,0,sizeof(dp));
for(int i=1;i<=N;i++)
for(int v=V;v>=c[i];v--)
dp[v]=max(dp[v],dp[v-c[i]]+w[i]);
空间复杂度 O ( V ) O(V) O(V)
题意:
每个物品没有价值只有体积,只要让体积尽可能的大即可
for(int i=1;i<=N;i++)
for(int v=V;v>=c[i];v--)
{
dp[v]=max(dp[v],dp[v-c[i]]+c[i]);
if(dp[V]=V)//一个重要优化,如果已经装满,则退出。去掉可能有点会超时。
break;
}
其实这个题最好用bitset,但是我不会……
-
0/1背包最小值
有N个物品,每个物品价值为w[i],体积为c[i],背包体积为V现在仅需要装入背包价值为k以上即可,求所需最小的体积(即留下的最大体积)。- 有两种方式,一是求不同体积时能够拥有的最大价值,再遍历一遍dp数组即可
- 二是……求不同价值时能够占用的最小体积,并处理无法刚好价值为k的情况。
①dp[i]求当前体积的最大价值
memset(dp,0,sizeof(dp));
dp[0]=0;
for(int i=1;i<=N;i++)
for(int j=V;j>=c[i];j--)//在j体积时,所拥有的最大价值。
dp[j]=max(dp[j],dp[j-c[i]]+w[i]);
for(int i=1;i<V;i++)
if(dp[i]>=k)//i体积时,拥有的价值大于或等于k,条件成立。
{
cout<<dp[i]<<" "<<i;//i即为最小花费体积
break;
}
②dp[i]求当前价值的最小体积
memset(dp,0x3f3f3f3f,sizeof(dp));
dp[0]=0;
for(int i=1;i<=N;i++)
for(int j=
/*
dp数组长度(不要用k代替,因为当无法刚好凑够k,即最终结果略大于所要求的价值k,会有问题
【k之后的值都没有初始化,后面while失效】【所以尽量还是不要用此方法】)
*/
;j>=w[i];j--)
dp[j]=min(dp[j],dp[j-w[i]]+c[i]);
while(k</*dp数组的长度*/)
{
if(dp[k]>=0x3f3f3f3f)
k++;
else
{
cout<<dp[k]<<endl;//即找到了总价值不小于k,花费的最小体积。
break;
}
}
-
完全背包
每个物品可以放无限多次
有n 种物品,第 i 件物品的体积是 c[i],价值是 w[i]。从这些物品中挑选总体积不超过V 的物品,求出挑选物品价值总和的最大值。在这里,每种物品可以挑选任意多件。
此问题和0/1不背包不同的是:此时每种物品有取0件、1件……多件的多种情况。而非上面仅两种情况——取或不取。
按照0/1背包问题可列出如下状态转移方程:
d
p
[
i
]
[
v
]
=
m
a
x
(
d
p
[
i
]
[
v
−
k
∗
c
[
i
]
]
+
k
∗
w
[
i
]
)
(
0
<
=
k
∗
c
[
i
]
<
=
v
)
dp[i][v]=max(dp[i][v-k*c[i]]+k*w[i]) (0<=k* c[i]<=v)
dp[i][v]=max(dp[i][v−k∗c[i]]+k∗w[i])(0<=k∗c[i]<=v)
注意此处
d
p
[
i
]
[
v
−
k
∗
c
[
i
]
]
+
k
∗
w
[
i
]
dp[i][v-k*c[i]]+k*w[i]
dp[i][v−k∗c[i]]+k∗w[i]而不是
d
p
[
i
−
1
]
[
v
−
k
∗
c
[
i
]
]
+
w
[
i
]
∗
k
dp[i-1][v-k*c[i]]+w[i]*k
dp[i−1][v−k∗c[i]]+w[i]∗k
因为考虑
i
i
i 时,之前可能已将放过第
i
i
i 个,之后还能继续放。
和0/1背包一样有
O
(
V
∗
N
)
O(V * N)
O(V∗N)种状态,但每种状态求解时间是
O
(
∑
i
=
1
n
v
c
[
i
]
)
O(\sum_{i=1}^{n}\frac{v}{c[i]})
O(∑i=1nc[i]v)
时间复杂度为
O
(
V
∗
∑
i
=
1
n
v
c
[
i
]
)
O(V* \sum_{i=1}^{n}\frac{v}{c[i]})
O(V∗∑i=1nc[i]v) ,比较大
一个无关紧要的简单有效优化:
两件物品
i
,
j
i,j
i,j,若
c
[
i
]
<
=
c
[
j
]
c[i]<=c[j]
c[i]<=c[j]&&
w
[
i
]
>
=
w
[
j
]
w[i]>=w[j]
w[i]>=w[j],则可以把物品
j
j
j去掉,
转化为0/1背包求解
第i种物品最多选
v
c
[
i
]
\frac{v}{c[i]}
c[i]v件,那就把第i件物品转化成
v
c
[
i
]
\frac{v}{c[i]}
c[i]v件体积为
c
[
i
]
c[i]
c[i],价值为
w
[
i
]
w[i]
w[i]的物品,然后求解0/1背包。这样虽然没有改进时间复杂度,但是提供了一种转化思路:将多件一种物品拆成多件不同物品。
memset(dp,0,sizeof(dp));
for(int i=1;i<=N;i++)
for(int v=0;v<=V;v++)
if(v>=c[i])
dp[i][v]=max(dp[i-1][v],dp[i-1][v-c[i]]+w[i]);
else dp[i][v]=dp[i-1][v];
根据上式发现,
d
p
[
i
]
[
v
]
只
和
i
−
1
行
和
i
dp[i][v]只和i-1行和i
dp[i][v]只和i−1行和i行的值有关,所以只用一个一维数组存储即可。
更优的
O
(
V
∗
N
)
O(V*N)
O(V∗N)的算法:
for(int i=0;i<=N;i++)
for(int v=c[i];v<=V;v++)
dp[v]=max(dp[v],dp[v-c[i]]+w[i]);
即0/1背包逆序第二重循环
0/1背包第二重循环从V到c[i]的原因是:dp[i][v]由dp[i-1][v-c[i]]推导而来,所以第i件物品一定还没有装入。
而现在物品有无限件,所以在考虑第i件物品时,正要一个可能已经选入第i件物品的情况。即v从c[[i]到V循环
-
多重背包
每种物品数量有限
有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件体积是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
状态转移方程:
d
p
[
i
]
[
v
]
=
m
a
x
(
d
p
[
i
−
1
]
[
v
−
k
∗
c
[
i
]
]
+
w
[
i
]
∗
k
)
(
0
<
=
k
<
=
n
[
i
]
)
dp[i][v]=max(dp[i-1][v-k*c[i]]+w[i]*k) (0<=k<=n[i])
dp[i][v]=max(dp[i−1][v−k∗c[i]]+w[i]∗k)(0<=k<=n[i])
第i种物品有选0件,1件……n[i]件共n[i+1]种策略
复杂度为
O
(
v
∗
∑
i
=
1
N
n
[
i
]
)
O(v* \sum_{i=1}^{N}n[i])
O(v∗∑i=1Nn[i])
转化为0/1背包问题,并利用二进制:
将第i种物品分成若干件物品,每件物品有一个系数,使这些系数分别为:
1
,
2
,
4
,
…
…
2
k
−
1
,
n
[
i
]
−
2
k
+
1
,
且
k
是
满
足
n
[
i
]
−
2
k
+
1
>
0
的
最
大
整
数
1,2,4,……2^{k-1},n[i]-2^k+1,且k是满足n[i]-2^k+1>0的最大整数
1,2,4,……2k−1,n[i]−2k+1,且k是满足n[i]−2k+1>0的最大整数,则一件物品的体积和价值均要乘这个系数。例如将n[i]=13拆成1,2,4,6。13以内的所有数字都可通过这四个数字组合而成。则最后等价于取若干件固定系数的物品。
这样将第i种物品分成了 O ( log 10 n [ i ] ) O(\log_{10}^{n[i]}) O(log10n[i])种物品,即转化成 O ( V ∗ ∑ i = 1 n [ i ] ) 的 0 / 1 O(V*\sum_{i=1}^{n[i]})的0/1 O(V∗∑i=1n[i])的0/1背包问题。
for(int i=1;i<==N;i++)
{
if(V<==n[i]*c[i])
{
//完全背包
for(int v=c[i];v<=V;v++)
dp[v]=max(dp[v],dp[v-c[i]]+w[i]);
}
else
{
int amount=1;
int number=n[i];
while(amout<number)
{
//0/1背包
for(int v=V;v>=amount*c[i];v--)
dp[v]=max(dp[v],dp[v-amount*c[i]]+w[i]*amount);
number=number-amount;
amount*=2;
}
//0/1背包
for(int v=V;v>=amount*c[i];v--)
dp[v]=max(dp[v],dp[v-c[i]*amount]]+amount*w[i]);
}
}
-
二维背包
【0/1二维背包】有N件物品,对于每件物品,具有两种不同的背包;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。两种代价可付出的最大值(两种背包容量)分别为J和K。物品的价值为c[i]。
状态转移方程:
d p [ i ] [ j ] [ k ] = m a x ( d p [ i − 1 ] [ j ] [ k ] , d p [ i − 1 ] [ j − a [ i ] ] [ k − b [ i ] ] + c [ i ] ) dp[i][j][k]=max(dp[i-1][j][k],dp[i-1][j-a[i]][k-b[i]]+c[i]) dp[i][j][k]=max(dp[i−1][j][k],dp[i−1][j−a[i]][k−b[i]]+c[i])
二维数组空间优化:
d
p
[
j
]
[
k
]
=
m
a
x
(
d
p
[
j
]
[
k
]
,
d
p
[
j
−
a
[
i
]
]
[
k
−
b
[
i
]
]
+
c
[
i
]
)
dp[j][k]=max(dp[j][k],dp[j-a[i]][k-b[i]]+c[i])
dp[j][k]=max(dp[j][k],dp[j−a[i]][k−b[i]]+c[i])
for(int i=1;i<=N;i++)
for(int j=J;j>=a[i];j--)
for(int k=K;k>=b[i];k--)
dp[j][k]=max(dp[j][k],dp[j-a[i]][k-b[i]]+c[i]);
cout<<dp[J][K];
当然,除了二维0/1背包,还有二维完全背包,二维多重背包等,只是循环条件的不同而已,此处不一一列举。