背包dp笔记

背包$DP$笔记

背包是线性DP中一类重要而特殊的模型,下面分几个板块讲述。

0/1背包

$0/1$背包问题模型如下:

给定$N$个物品,其中第$i$个物品的体积为$v_i$,价值为$w_i$。有一容积为$M$的背包,要求选择一些物品放入背包,是得物品总体积不超过$M$的前提下,物品的价值总和最大。

根据以前线性$DP$的知识,很容易想到依次考虑每个物品是否放入背包,用“已经处理的物品数”作为$DP$的“阶段”,以“背包中已经放入物品的总体积”作为附加维度。

$F[i,j]$表示从前$i$个物品中选出总体积为$j$的物品放入背包,物品的最大价值和。

显然,状态转移方程如下:

$F[i][j]=\text{max} \left\{ \begin{aligned} &\text{F[i-1][j]——不选第$i$个物品}\\ &\text{F[i-1][$j-v_i$]+$w_i$} ——\text{选第$i$个物品} \end{aligned} \right. $

边界情况:$F[0,0] = 0$,其余均为负无穷。目标:$ \mathop{\max}\limits_{0\leq j \leq M} \{F[N][j]\}$

时间复杂度$O(NM)$

关键代码如下:

memset(f,0x3f,sizeof(f));
f[0][0]=0;
for(int i=1;i<=n;i++){
    for(int j=0;j<=m;j++)
        f[i][j]=f[i-1][j];
    for(int j=v[i];j<=m;j++)
        f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
}

滚动数组优化:注意循环枚举的顺序

int f[MAX_M+1];
memset(f,0x3f,sizeof(f));
f[0]=0;
for(int i=1;i<=n;i++){
    for(int j=m;j>=v[i];j--){
        f[j]=max(f[j],f[j-v[i]]+w[i]]);
    }
}
int ans=0;
for(int i=0;i<=m;i++)ans=max(ans,f[i]);

例题:数字组合

给定 $N$个正整数 $A_1,A_2,…,A_N$,从中选出若干个数,使它们的和为$M$,求有多少种选择方案。

数据范围:$1\leq N \leq 100$

​ $1 \leq M \leq 10000$

​ $1 \leq A_i \leq 1000$

这是一个典型的$0/1$背包模型,$N$个正整数就是$N$个物品,$M$就是背包的容积。在外层循环到$i$时(表示从前$i$个数中选),设$F[j]$表示“和为$j$”有多少种方案。在具体实现中,只需要把上面代码中求$max$的函数改为求和即可。

核心代码如下:

int f[MAX_M+1];
memset(f,0,sizeof(f));
f[0]=1;
for(int i=1;i<=n;i++){
    for(int j=m;j>=a[i];j--){
        f[j]+=f[j-a[i]];
    }
}
cout<<f[m]<<endl;

完全背包

完全背包问题模型如下:

给定$N$物品,其中第$i$个物品的体积为$v_i$,价值为$w_i$,并且有无数个。有一容积为$M$的背包,要求选择若干个物品放入背包,是得物品总体积不超过$M$的前提下,物品的价值总和最大。

先来考虑使用传统的二维线性$DP$的做法,设$F[i,j]$表示从前$i$物品中选出了总体积为$j$的物品放入背包,物品的最大价值和。

$ F[i][j]=\text{max} \left\{ \begin{aligned} &\text{F[i-1][j]——尚未选过第$i$种物品}\\ &\text{F[i-1][$j-v_i$]+$w_i$}(if j \geq v_i)——\text{从第i种物品中选一个} \end{aligned} \right. $

初值:$F[0,0]=0$,其余均为负无穷。目标:$ \mathop{\max}\limits_{0\leq j \leq M} \{F[N][j]\}$

与$0/1$背包一样。我们也可以省略$F$数组的$i$这一维。根据我们在$0/1$背包中对循环顺序的分析,当采用正序循环时,就对应着每种物品可以用无限次,也对应着$F[i,j]=F[i,j-v_i]+w_i$这个在两个均处于$i$阶段的状态之间进行转移的方程。 关键代码如下:

int f[MAX_M+1];
memset(f,0xcf,sizeof(f));
f[0]=0;
for(int i=1;i<=n;i++)
    for(int j=v[i];j<=m;j++)
        f[j]=max(f[j],f[j-v[i]]+w[i]);
int ans=0;
for(int j=0;j<=m;j++)
    ans=max(ans,f[j]); 

例题:

给定一个自然数$N$,要求把$N$拆分成若干个正整数相加的形式,参与加法运算的数可以重复。求拆分的方案数$mod$ $2147483648$的结果。$1\leq N \leq 4000$

这是一个典型的完全背包模型,$1 \sim N$这$N$个自然数构成$N$种物品,每种物品都可以无限次使用,背包容积也是$N$。与上一题类似,本题也是要求方案数,我们在完全背包程序模板的基础上,把求$max$的函数改为求和即可。

f[0]=1;
for(int i=1;i<=n;i++)
    for(int j=i;j<=n;j++)
       f[j]=(f[j]+f[j-i])%mod;
printf("%lld\n",f[n]%mod-1);

多重背包

多重背包问题模型如下:

给定$N$物品,其中第$i$种物品的体积为$v_i$,价值为$w_i$,并且有$c_i$个。有一容积为$M$的背包,要求选择若干个物品放入背包,是得物品总体积不超过$M$的前提下,物品的价值总和最大。

直接拆分法

求解多重背包问题最直接的方法就是把第$i$中物品看作独立的$c_i$个物品,转化为$\sum_{i=1}^{N}c_i$个物品的$0/1$背包问题进行计算,时间复杂度为$O(M*\sum_{i=1}^{N}c_i)$。该算法把每种物品拆分成了$c_i$个,效率较低。

unsigned int f[MAX_M+1];
memset(f,0xcf,sizeof(f));
f[0]=0;
for(int i=1;i<=n;i++)
    for(int j=1;j<=c[i];j++)
        for(int k=m;k>=v[i];k--)
            f[k]=max(f[k],f[k-v[i]]+w[i]);
int ans=0;
for(int i=0;i<=m;i++)ans=max(ans,f[i]);

二进制拆分法

众所周知,从$2^0, 2^1, 2^2, ...,2^{k-1}$这$k$个$2$的整数次幂选出若干个相加,可以表示出$0 \sim 2^{k-1}$之间的任何整数。进一步地,我们求出满足$2^0, 2^1, 2^2, ...,2^{p}$ $\leq c_i$的最大的整数$p$,设$r_i=c_i-2^0-2^1-...-2^p$,那么:

1.根据$p$的最大性,有$2^0, 2^1, 2^2, ...,2^{p+1}>c_i$,可推出$2^{p+1}>r_i$,因此$2^0, 2^1, 2^2, ...,2^{p}$选出若干个相加可以表示出$0 \sim r_i$之间的任意整数。

2.从$2^0, 2^1, 2^2, ...,2^{p}$以及$r_i$中选出若干个相加,可以表示出$r_i \sim r_i+2^{p+1}-1$之间的任何整数,根据$r_i$的定义,$r_i+2^{p+1}-1=c_i$,因此$2^0, 2^1, 2^2, ...,2^{p},r_i$选出若干个相加可以表示出$r_i \sim c_i$之间的任何整数。

综上所述,,我们可以把数量为$c_i$的第$i$种物品拆成$p+2$个物品,它们的体积分别为:

$2^0*v_i, 2^1*v_i, 2^2*v_i, ...,2^{p}*v_i,r_i*v_i$

这$p+2$个物品可以凑成$0 \sim c_i*v_i$之间所有能被$v_i$整除的数,并且不能凑成大于$c_i*v_i$的数。这等价于原问题中体积为$v_i$的物品可以使用$0 \sim c_i$次。该方法仅把每种物品拆成了$O(log c_i)$个,效率较高。

posted @ 2023-12-11 20:35  CQWYB  阅读(3)  评论(1编辑  收藏  举报  来源