背包问题大汇总

@[toc]

01背包

问题:

有N件物品和一个容量为V的背包,第i件物品的费用(体积)是w[i],价值是c[i],求解将哪些物品装入背包可使这些物品的费用综合不超过背包容量,且价值总和最大

思路:

f[i][v]表示前i件物品(部分或全部)恰放入一个容量为v的背包可以获得的最大价值。
转移方程:
f [ i ][ v ] = m a x ( f [i - 1 ] [ v ] , f [i - 1 ] [ v -w [ i ] ] + c [ i ] )
这是最基础的背包dp,我们在考虑dp问题的时候,这么想,f[i][v]的状态是怎么来的?也就是第i件物品选择方式?有两种方式,第一种就是第i件商品不拿,那么f[i][v]的状态就是前i-1件物品恰放入一个容量为v的背包(即f[i-1][v])直接转移过来的,另外一种就是第i件商品选择拿,那么f[i][v]的状态就是前i-1件物品恰放入一个容量为v-w[i]的背包(即f[i-1][v-w[i]])再加入第i件商品(c[i]),其实就是末状态(f[i][v])将第i件商品去除,可以得到初状态(f[i-1][v-w[i]])
本质就是由已知推未知
这样最后答案是max(f[N][0.......V]),因为我们在定义状态f时说的“恰”,也就是最佳情况并不一定是背包要装满,所以要取各种状态的最大值。
怎么样这个“恰”去掉呢?我们可以将所有f[i][j]初始化为0,也就是最开始的f[i][j]是有意义的,只是价值为0,可以看做无物品的背包价值都是0,这样最后的结果就是f[N][V]

空间优化复杂度

我们可以用一维的f[i]来代替上面的二维数组
for i=1.....N
for v=V....0
f [v ] = max ( f[ v ] , f[ v- w [ i ] ] + c [ i ] )
我们在每次主循环时用v=V...0的逆序推f[v],这样才能保证推f[v]时f[v-w[i]]保存的是状态f[i-1][v-w[i]]的值。

代码

二维:

//f[i][v]表示前i件物品,总质量不超过v的最优价值 
for(int i=1;i<=N;i++)
{
    for(int v=V;v>0;v--)
    {
        if(w[i]<=v)f[i][v]=max(f[i-1][v],f[i-1][v-w[i]]+c[i]);//如果能装下 
        else f[i][v]=f[i-1][v];
    }
}
printf("%d",f[N][V]);

一维:

//f[v]表示质量不超过v公斤的最大价值
for(int i=1;i<=N;i++)
{
    for(int v=V;v>=w[i];v--)//避免数组出现负下标
    {
        f[v]=max(f[v],f[v-w[i]]+c[i]);
    }
} 
printf("%d",f[V]);

完全背包

问题:

有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是w[i],价值是c[i].求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

思路:

和01背包的区别在于,每件物品不再只有取和不取两种状态,而是有取0件,取1件,取2件..........很多种。
我们用01背包的方式来做,f[i][v]表示前i种物品恰好放入一个质量为v的背包的最大权值
f [ i ] [ v ] = m a x (f [ i - 1 ] [ v - k * w [i ] ] + k * c [ i ] | 0 < =k * w [ i ] <= v )
使用一维方程:
for i=1...N
for v=0.....V
f [v ] = m a x (f [ v ] , f[ v - w [i ] ] +c [ i ] )、
你会发现与01背包相比,仅仅是把v的枚举顺序倒了过来
为啥呢?
01背包中v是从V->0,这是因为为了保证第i次循环中的状态f[i][v]是由上一个状态f[i-1][v-w[i]]递推而来,这样可以确定每个物品只选一次,我们在考虑“加选第i件物品”这一策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-w[i]]
而完全背包中物品的数量是无限的,所以在考虑“加选第i件物品”时,需要的是一个已经选入第i种物品的子结果f[i][v-w[i]],也就是再已放入第i种物品的基础上再进行转移,所以要采用v=0->V的顺序
二维方程的话:
f [ i ] [ v ] = max ( f [ i - 1 ] [ v ] , f[ i ] [ v - w [ i ] ]+ c [ i ] )

代码:

f[i][v]表示前i件物品,总质量不超过v的最优价值

for(int i=1;i<=N;i++)
{
    for(int v=0;v<=V;v++)
    {
        if(v<w[i])f[i][v]=f[i-1][v];
        else 
        {
            f[i][v]=max(f[i-1][v],f[i][v-w[i]]+c[i]);
        }
    }

}
printf("%d",f[N][V]);

f[v]表示质量不超过v公斤的最大价值

for(int i=1;i<=N;i++)
{
    for(int v=0;v<=V;v++)
    {
        if(v>w[i])f[v]=max(f[v-w[i]]+c[i],f[v]); 
    }
}
printf("%d",f[V]);

优化

完全背包中,如果两个物品i与j,w[i]<=w[j]&&c[i]>=c[j],物品j就可以去掉,也就是一个物品占空间又大,价值还低,那要他干啥,j完全可以被i代替。这是一个简单的小优化
我们还可以把完全背包问题转化成01背包问题来解
因为背包容量是给定的,所以就可以算出每件物品最多可以装多少,将无限数量转变成有限数量,用01背包去做
但是这样的转变也太low了,对程序优化不了多少,更高效的转化是:把第i种物品拆成费用为w[i]2^k^,价值拆成c[i]w^k^的若干件物品,k满足w[i]*2^k^<V。二进制拆分的原理我们可以用 1,2,4,8...2n 表示出1 到 2n+1−1的所有数.利用二进制的思想来优化问题,不管最优策略是选几件第i种商品,总可以写成若干个2^k^件物品的和。把每件物品拆成O(log(V/w[i])+1)件物品

多重背包

问题:

有N种物品和一个容量为V的背包,第i种物品最多有n[i]件可用,每件费用是w[i],价值是c[i].求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

思路:

和完全背包很相似,区别在于每个物品的数量是明确的是固定的,那直接就可以把完全背包的式子拿过来
f [ i ] [ v ] = m a x (f [ i - 1 ] [ v - k * w [i ] ] + k * c [ i ] | 0 < =k * <= n[i] )
一维的就是 f [ v] = max ( f[ v ] , f [ v - k * w [ i] ] + k* c [ i ] );
区别是k的范围是0~n[i]
但是光能这么做怎么够,我们看看怎么转化成最基础的01背包
最简单粗暴的就是你吧第i件物品换成n[i]件01背包中物品,就是把一种多件看作是多种一件,但是这样复杂度不变O(Vsum(n[i])),意义不大,怎么才能降低负责度
完全背包我们讲了二进制的方式来优化,这里我们也考虑二进制的思想。把第i种物品换成若干件物品,每个物品有一个系数,这件物品的费用和价值等于原本物品的乘以系数,而这些系数分别是2的k次方(系数为0,2,4,),最后一个系数是n[i]-2^k^+1(剩下的)。例如13就可以拆分成系数为1,2,4,6(因为8达不到,所以用13减去前面的1,2,4,得到6)的四件物品
这样就将第i种物品分成O(logn[i])种物品,将原问题转化为了复杂度为O(V
sum(logn[i])的01背包问题

代码:

朴素算法:

for(int i=1;i<=n;i++)
{
    for(int j=V;j>=0;j--)
    {
        for(int k=0;k<=n[i];k++)
        {
            if(j-k*w[i]<0)break;//如果当前情况容量不足,那后面容量肯定也不够,直接跳出换成其他物品 
            f[j]=max(f[j],f[j-k*w[i]]+k*c[i]);
        }
    }
}
cout<<f[V];

进行二进制优化,转化成01背包

int main()
{
    cin>>n>>V;
    for(int i=1;i<=n;i++)//把一种物品的数量按照二进制进行拆分,并乘以对应系数 
    {
        int x,y,s,t=1;
        cin>>x>>y>>s;//输入费用,单价值,数量 
        while(s>=t)
        {
            n1++;
            w[n1]=x*t;//乘以系数 
            c[n1]=y*t;
            s-=t;
            t*=2;
        }
        w[++n1]=x*s;//剩余费用 
        c[n1]=y*s;//剩余价值 
    }
    for(int i=1;i<=n1;i++)
    {
        for(int j=V;j>=w[i];j--)
        {
            f[j]=max(f[j],f[j-w[i]]+c[i]);//01背包解法 
        }
    }
    cout<<f[V];
} 
for(int i=1;i<=n;i++)
{
    for(int j=1;j<=num[i];j<<=1)
    //二进制每一位枚举.
    //注意要从小到大拆分
    {
        num[i]-=j;//减去拆分出来的
        new_c[++tot]=j*c[i];//合成一个大的物品的体积
        new_w[tot]=j*w[i];//合成一个大的物品的价值
    }
    if(num[i])//判断是否会有余下的部分.
    //就好像我们某一件物品为13,显然拆成二进制为1,2,4.
    //我们余出来的部分为6,所以需要再来一份.
    {
        new_c[++tot]=num[i]*c[i];
        new_w[tot]=num[i]*w[i];
        num[i]=0;
    }
}

单调队列优化

多重背包最普通的状态方程:
f [ i ][ j ] = max ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − k ∗ c [ i ] ] + k∗ w [ i ] )  

混合三种背包

问题:

如果把01背包,完全背包,多重背包混合起来。也就是说,有的物品只可以去一次(01背包),有的物品可以无限取(完全背包),有的物品可以取的次数有一个上限(多重背包),应该怎么求解呢?

思路

01背包和完全背包代码中只有v的循环顺序不同,所以可以针对不同的商品类型选用顺序或者逆序的循环即可
for i=1....N
if 第i件物品是01背包
for v = V.....0
f [ v ] = max ( f [ v] , f [ v - w [ i ] ] + c [ i ] )
else if 第i件物品是完全背包
for v=0........V
f [ v ] = max ( f [ v ] , f [ v- w [ i ] ] + c [ i ] )
再加上多重背包
将多重背包分成多个01背包然后求解

例题

一个容量为m的背包,现在有n件物品,质量分别是W[i],价值分别是C[i],P[i]表示物品的数量,(0说明可以购买无数次,其他数字为购买的最多件数)

代码:

for(int i=1;i<=n;i++)
{
    if(完全背包) 
    {
        for(int j=c[i];j<=V;j++)
            f[j]=max(f[j],f[j-c[i]]+w[i]);
    }
    else if(01背包) 
    {
        for(int j=V;j>=c[i];j--)
            f[j]=max(f[j],f[j-c[i]]+w[i]); 
    }
    else//否则就是完全背包了 
    {
        for(int j=V;j>=0;j--)
            for(int k=1;k<=num[i];k++)
                if(j-k*c[i]>=0)
                    f[j]=max(f[j],f[j-k*c[i]]+k*w[i]);
    }
}
int main()
{
    cin>>m>>n;
    for(int i=1;i<=n;i++)cin>>w[i]>>c[i]>>p[i];
    for(int i=1;i<=n;i++)
    {
        if(p[i]==0)//完全背包
        {
            for(int j=w[i];j<=m;j++)
            f[j]=max(f[j],f[j-w[j]]+c[i]);
        }
        else 
        {
            for(int j=1;j<=p[i];j++)//01背包和多重背包 
            {
                for(int k=m;k>=w[i];k--)
                {
                    f[k]=max(f[k],f[k-w[i]]+c[i]);
                }
            }
        } 
    }
    cout<<f[m];
} 
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e4+7;
int w[maxn],v[maxn],tot[maxn];
int dp[200010];
int main()
{
    int V,n;
    scanf("%d%d",&n,&V);
    for(int i=1; i<=n; i++)
        scanf("%d%d%d",&w[i],&v[i],&tot[i]);
    for(int i=1; i<=n; i++)
    {
        if(tot[i]==0)///完全背包
            for(int j=w[i]; j<=V; j++)
                dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
        else///01与多重背包(二进制优化后的多重背包与01背包循环非常相似)
        {
            int x=tot[i];
            for(int k=1; k<x; k<<=1)///此处套用二进制优化的模板
            {
                for(int j=V; j>=w[i]*k; j--)
                    dp[j]=max(dp[j],dp[j-w[i]*k]+v[i]*k);
                x-=k;
            }
            if(x)
                for(int j=V; j>=w[i]*x; j--)///相当于还剩一个物品未考虑
                    dp[j]=max(dp[j],dp[j-w[i]*x]+v[i]*x);
        }
    }
    cout<<dp[V];
    return 0;
}

二维费用的背包问题

分组的背包问题

有依赖的背包问题

背包问题的方案总数