背包九讲

https://blog.csdn.net/tinyguyyy/article/details/51203935

https://blog.csdn.net/qq_34374664/article/details/52230368

《背包九讲》https://max.book118.com/html/2017/0615/115637327.shtm

背包问题是典型的DP问题,几乎所有类型的背包问题都可转化为DP运算。

一.01背包

这是最基础的背包问题

特点:每种物品仅有一件,可以选择放或不放。

题目:

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

用子问题定义状态:

f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。

状态转移方程:f[i][v]=max{ f[i-1][v] , f[i-1][v-c[i]]+w[i] }

将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为j的背包中”,价值为f[i-1][v];如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,此时能获得的最大价值就是f[i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]。

优化空间复杂度

以上方法的时间和空间复杂度均为O(N*V),其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到O(V)。

伪代码:

f[0...V]=0
for i=1 to N
for v=V to ci(注意是逆序,以保证所有物品只能选一次)
f[v]=max{ f[v] , f[v-c[i]]+w[i] };

初始化的细节问题

我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。①求“恰好装满背包”时的最优解,②则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。

https://www.cnblogs.com/buddho/p/7867920.html

http://blog.sina.com.cn/s/blog_150cffdab0102w0g9.html

①第一种问法,要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。(0x3f3f3f3f表无穷大,0xc0c0c0c0表无穷小  //1061109567, -1061109568

②如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设为0

Why?

    可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的情况下被“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。

这个小技巧完全可以推广到其它类型的背包问题,后面也就不再对进行状态转移之前的初始化进行讲解。


#include<bits/stdc++.h>

using namespace std;

int dp[1005][1005];

int weight[1005];

int value[1005];

int main()

{

    int n,m;

    cin>>m>>n;

    memset(dp,0,sizeof(dp));//数组清空,其实同时就把边界给做了清理

    for(int i=1; i<=n; i++)

        cin>>weight[i]>>value[i];

    //从1开始有讲究的因为涉及到dp[i-1][j],从0开始会越界

    for(int i=1; i<=n; i++)//判断每个物品能否放进

    {

        for(int j=0; j<=m; j++)//对每个状态进行判断

        //这边两重for都可以倒着写,只是需要处理最边界的情况,滚动数组不一样

        {

            if(j>=weight[i])//能放进

                dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);

 

            else dp[i][j]=dp[i-1][j];//不能放进

        }

    }

    cout<<dp[n][m]<<endl;

    return 0;

}

!!!!空间复杂度优化后:

用滚动数组

说白了二维数组只是把每个物品都跑一遍,然后到最后一个物品的时候输出答案,那么过程值只是计算的时候用一次,我没必要存下来。所以用一个数组去滚动存储,然后用后一个状态的值去覆盖前面一个状态。然后形象的叫它:滚动数组(ma!dan!一点都不形象,我理解了好久)

好吧,假装很形象。

那么问题来了,怎么样用一维的去代替二维的工作,或者说怎么去思考。这是一个难点。

那么我们想,遍历物品的那个for肯定不能省去,然后里边的for也不能省。。。。那么。就把那个i给他删了吧,好像确实没啥用哦。

然后就出现了这样的代码


#include<bits/stdc++.h>

using namespace std;

int dp[1005];//滚动数组的写法,省下空间省不去时间

int weight[1005];

int value[1005];

int main()

{

    int n,m;

    cin>>m>>n;

    memset(dp,0,sizeof(dp));

    for(int i=1; i<=n; i++)

        cin>>weight[i]>>value[i];

    for(int i=1; i<=n; i++)//对每个数判断,可反

    {

        for(int j=m; j>=weight[i]; j--)//这里这个循环定死,不能反,反了就是完全背包

        {

            dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);//其实不断在判断最优解,一层一层的

        }

    }

    cout<<dp[m]<<endl;

    return 0;

}

其实就是规定从m开始循环,保证了选择这个物品时,肯定不会重复使用状态。

从下到上,从右往左

二.完全背包

特点:每种物品有无限件


#include<bits/stdc++.h>

using namespace std;

int dp[100005];//m

struct Node{

    int a,b;

}node[1005];//n

 

int main(){

    int n;

    while(~scanf("%d",&n)){

        for(int i=0;i<n;i++){

            scanf("%d%d",&node[i].a,&node[i].b);

        }

        int m;

        scanf("%d",&m);

        memset(dp,0,sizeof(dp));

        for(int i=0;i<n;i++){

            for(int j=node[i].b;j<=m;j++){//这样就是完全背包

                dp[j]=max(dp[j],dp[j-node[i].b]+node[i].a);

            }

        }

        printf("%d\n",dp[m]);

    }

    return 0;

}

三.多重背包

特点:每种物品有有限件num[i]

可以把物品拆开,把相同的num[i]件物品 看成 价值跟重量相同的num[i]件不同的物品


#include<bits/stdc++.h>

using namespace std;

int dp[1005];

int weight[1005],value[1005],num[1005];

int main()

{

    int n,m;

    cin>>n>>m;

    memset(dp,0,sizeof(dp));

    for(int i=1; i<=n; i++)

        cin>>weight[i]>>value[i]>>num[i];

        

    for(int i=1; i<=n; i++)//每种物品

        

        for(int k=0; k<num[i]; k++)//其实就是把这类物品展开,调用num[i]次01背包代码

        

            for(int j=m; j>=weight[i]; j--)//正常的01背包代码

            

                dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);

    

    cout<<dp[m]<<endl;

    return 0;

}

#include<bits/stdc++.h>

using namespace std;

const int N = 1005;

 

int dp[N];

int c[N],w[N],num[N];

int n,m;

 

void ZeroOne_Pack(int cost,int weight,int n)//吧01背包封装成函数

{

    for(int i=n; i>=cost; i--)

        dp[i] = max(dp[i],dp[i-cost] + weight);

}

 

void Complete_Pack(int cost,int weight,int n)//把完全背包封装成函数

{

    for(int i=cost; i<=n; i++)

        dp[i] = max(dp[i],dp[i-cost] + weight);

}

 

int Multi_Pack(int c[],int w[],int num[],int n,int m)//多重背包

{

    memset(dp,0,sizeof(dp));

    for(int i=1; i<=n; i++)//遍历每种物品

    {

        if(num[i]*c[i] > m)

            Complete_Pack(c[i],w[i],m);

            //如果全装进去已经超了重量,相当于这个物品就是无限的

            //因为是取不光的。那么就用完全背包去套

        else

        {

            int k = 1;

            //取得光的话,去遍历每种取法

            //这里用到是二进制思想,降低了复杂度

            //为什么呢,因为他取的1,2,4,8...与余数个该物品,打包成一个大型的该物品

            //这样足够凑出了从0-k个该物品取法

            //把复杂度从k变成了logk

            //如k=11,则有1,2,4,4,足够凑出0-11个该物品的取法

            while(k < num[i])

            {

                ZeroOne_Pack(k*c[i],k*w[i],m);

                num[i] -= k;

                k <<= 1;

            }

            ZeroOne_Pack(num[i]*c[i],num[i]*w[i],m);

        }

    }

    return dp[m];

}

 

int main()

{

    int t;

    cin>>t;

    while(t--)

    {

        cin>>m>>n;

        for(int i=1; i<=n; i++)

            cin>>c[i]>>w[i]>>num[i];

        cout<<Multi_Pack(c,w,num,n,m)<<endl;

    }

    return 0;

}

 

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值