刷题(8)-背包问题(3)

目录

 

背包问题

1. 01背包

采用空间压缩的代码(且用方案二的初始化方法):

2.完全背包

代码:

3. 多重背包

3.1 朴素办法:

3.2 利用二进制进行优化 O(V*n*logn(i))

代码:

3.3 单调队列优化

4.混合背包

代码

5.二维费用的背包问题

01背包的二维费用代码:

6.分组的背包问题 

01背包的分组背包代码

7.01背包问题求最优方案数

方案1代码:

方案二代码:

8.背包问题求具体方案

输出字典序代码:

9.有依赖的背包问题


背包问题

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

所有的背包问题都是先循环物品,再循环体积,再循环决策

https://www.cnblogs.com/jbelial/articles/2116074.html

1. 01背包

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

记忆方法: 先枚举物品,再从大到小枚举体积

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件物品放入容量为v的背包中”;如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,此时能获得的最大价值就是f [i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]。 

tips:1. 当要求容量v是恰好装满时,初始化f[0,0]=0 , f[0,1]=f[0,2]=.......=f[0,n]=-∞(说明:因为前一个0代表什么物品都不能选,而后一个v代表此时背包被占用容量恰好是V,那自然是不可能的,所以用负无穷表示没有方案可以满足要求。)

     2. 当不要求恰好装满,初始化f[0,0]=f[0,1]=.......=f[0,n]=0 (因为这里的v表示背包最多能被占多少容量,f【0】【x】的含义是,什么物品都不能选,而背包最多能被放入总共容量为V的物品,此时的最大价值,自然是0)

所以如果用方案一的初始化方法,最大价值是 f【N】【0...V】的最大值,而如果用方案二的初始化方法,最大价值是f【N】【V】

采用空间压缩的代码(且用方案二的初始化方法):

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

int main()
{
    cin >> n >> m; // n表示物品总数 ,m表示背包容量

    for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];  //v[i]:第i件物品的体积 w[i]:第i件物品的价值

    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]);

    cout << f[m] << endl;

    return 0;
}

2.完全背包

每种物品都有无限件可用

记忆方法: 先枚举物品,再从小到大枚举体积

从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。如果仍然按照解01背包时的思路,令f[i][v]表示前i种物品放入一个容量为v的背包的最大价值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:

                                         f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<= v}

解释:前i种物品放入一个容量为v的背包的最大价值等于:

  1.  我完全不用第i件物品, f【i-1】【v】
  2.  我只用1件第i件物品, f【i-1】【v-c【i】】+w【i】
  3.  我只用2件第i件物品, f【i-1】【v-2c【i】】+2w【i】
  4.   ......
  5. 我只用k件第i件物品,  f【i-1】【v-kc【i】】+kw【i】(k是总容量为v,最多能放几件第i件物品)

f【i】【v】就等于这么多种情况的最大值,而其实第2种到第5种情况他们之间的最大值,其实就是

                                                    f【i】【v-c【i】】+w【i】(假如v小于c【i】,值应该等于0)

所以f【i】【v】=max( f【i-1】【v】,(v-c【i】)>=0? f【i】【v-c【i】】+w【i】: 0)

再进一步,假如采用空间压缩技巧,只要第二个循环枚举容量的时候,从小到大(而不是从大到小),就ok了

(同样,初始化的方法,决定最大值是哪个)

代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N];

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i];

    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]);

    cout << f[m] << endl;

    return 0;
}

3. 多重背包

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

记忆方法:先枚举物品,再从大到小枚举体积,最后从1开始枚举第i件物品放几件(需要满足自身限制和体积限制)

3.1 朴素办法:

令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值,则:f[i][v]=max{f[i-1][v-k*c[i]]+ k*w[i]|0<=k<=n[i]}。复杂度是O(V*∑n[i])。 (其中k既要满足 kc【i】<v,又要小于等于物品本身的限制k<n【i】)

tips:这里不能像完全背包一样进行优化,因为你无法知道f【i】【v-c【i】】有没有把第i种物品全给用光了

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 110;

int n, m;
int v[N], w[N], s[N];
int f[N];

int main()
{
    cin >> n >> m;

    for (int i = 1; i <= n; i ++ ) cin >> v[i] >> w[i] >> s[i];

    for (int i = 1; i <= n; i ++ )
        for (int j = m; j >=v[i] ; j -- )  //  体积需要从大到小枚举
            for (int k = 1; k <= s[i] && k * v[i] <= j; k ++ )
                f[j] = max(f[j], f[j - k * v[i] ] + k*w[i] );

    cout << f[m] << endl;
    return 0;
}

3.2 利用二进制进行优化 O(V*n*logn(i))

方法是:将第i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为 1,2,4,...,2^(k-1),n[i]-2^k+1,且k是满足n[i]-2^k+1>0的最大整数。例如,如果n[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。 分成的这几件物品的系数和为n[i],表明不可能取多于n[i]件的第i种物品。另外这种方法也能保证对于0..n[i]间的每一个整数,均可以用若干个系数的和表示。

这样就将第i种物品分成了O(log n[i])种物品,将原问题转化为了复杂度为O(V*∑log n[i])的01背包问题,是很大的改进。 

(将第i种物品,用二进制的方法分成若干件物品,这若干件物品选与不选,恰好可以组成0..n[i]间的每一个整数)

代码:

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 11010, M = 2010;

int n, m;
int v[N], w[N];
int f[N];

int main()
{
    cin >> n >> m;

    int cnt = 0;
    for (int i = 1; i <= n; i ++ )
    {
        int a, b, s;
        cin >> a >> b >> s;
        int k = 1;
        while (k <= s)
        {
            cnt ++ ;
            v[cnt] = a * k;
            w[cnt] = b * k;
            s -= k;
            k *= 2;
        }
        if (s > 0)
        {
            cnt ++ ;
            v[cnt] = a * s;
            w[cnt] = b * s;
        }
    }

    n = cnt;

    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]);

    cout << f[m] << endl;

    return 0;
}

3.3 单调队列优化

 

4.混合背包

有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。

解决方法:物品是什么类型的,就用什么方法求解。

具体的就是01背包用01背包的方法求(容量从大到小),完全背包用容量从小到大的方法求,而多重背包用容量从大到小,个数从1到极限求(也可以用二进制方法转为01背包)

代码

//01 背包则直接放入数据容器中
多重背包则化解成 01 背包 放入数据容器中(见多重背包II习题 进行二进制优化)
完全背包也直接放入数据容器中

此刻数据容器vector[HTML_REMOVED] things;中就只有01背包和完全背包 那么就进行遍历处理

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 1010;

int n,m;
int f[N];

struct Thing{
    int kind;
    int v,w;
};
vector<Thing> things;


int main()
{
    cin >> n>>m;
    for(int i = 0;i<n;i++)
    {
        int v,w,s;
        cin >> v >> w>> s;
        if(s < 0)
        {
            things.push_back({-1,v,w});      // 01背包
        }else if(s == 0) things.push_back({0,v,w}); // 完全背包
        else{
            for(int k = 1;k <= s; k*=2){
                s -=k;
                things.push_back({-1,v*k,w*k});
            }
            if(s > 0) things.push_back({-1,v*s,w*s});
        }
    }

    for(auto thing:things)
    {
        // 01 背包容量从大到小

        if(thing.kind < 0){
            for(int j = m;j >= thing.v;j--) f[j] = max(f[j],f[j-thing.v]+thing.w); 
        }
        
        // 完全背包从小到大
        else{
            for(int j = thing.v;j <= m;j++) f[j] = max(f[j],f[j-thing.v]+thing.w);
        }
    }

    cout << f[m] << endl;

    return 0;
}

5.二维费用的背包问题

二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为w[i]。 

解决方法:

费用加了一维,只需状态也加一维即可。设f[i][v][u]表示前i件物品付出两种代价分别为v和u时可获得的最大价值。状态转移方程就是:f [i][v][u]=max{f[i-1][v][u],f[i-1][v-a[i]][u-b[i]]+w[i]}。而如果使用空间压缩,可以只使用两维数组 f【v】【u】:假如是01背包(物品只可以取一次时)  变量v和u采用从大到小的两重循环,当物品是完全背包问题时采用从小到大的两重循环。当物品有如多重背包问题时拆分物品。

物品总个数的限制:
有时,“二维费用”的条件是以这样一种隐含的方式给出的:最多只能取M件物品。这事实上相当于每件物品多了一种“件数”的费用,每个物品的件数费用均为1,可以付出的最大件数费用为M。换句话说,设f[v][m]表示付出费用v、最多选m件时可得到的最大价值,则根据物品的类型(01、完全、多重)用不同的方法循环更新,最后答案就是f[V][M]。 

另外,如果要求“恰取M件物品”,则注意初始化方法,

01背包的二维费用代码:

//类似01背包问题用滚动数组,那么这个题目就只用二维数组就行了.
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int main(void) {

    int N, v, m;//物品种类  背包体积 背包载重

    cin >> N >> v >> m;
    vector<vector<int>>dp(v + 1, vector<int>(m + 1,0));

    for (int i = 1; i <= N; ++i) {
        int a,b,c; //物品的体积,质量,价值
        cin >> a >> b >> c;
        //每个物品只能用一次,从大到小
        for (int j = v; j >= a; --j) {
            for (int k = m; k >= b; --k) {
                dp[j][k] = max(dp[j][k], dp[j - a][k - b] + c);
            }
        }
    }

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

    return 0;
}

6.分组的背包问题 

问题:
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。 

算法 
这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设f[k][v]表示前k组物品花费费用v能取得的最大权值,则有f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于第k组}。 

使用一维数组的伪代码如下: 
for 所有的组k 
    for v=V..0 

        for 所有的i属于组k 
             f[v]=max{f[v],f[v-c[i]]+w[i]} 

另外,显然可以对每组中的物品应用P02中“一个简单有效的优化”。

注:优化  
完全背包问题有一个很简单有效的优化,是这样的:若两件物品i、j满足c[i]<=c[j]且w[i]>=w[j],则将物品j去掉,不用考虑。这个优化的正确性显然:任何情况下都可将价值小费用高得j换成物美价廉的i,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。 

01背包的分组背包代码

// 分组背包问题
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 110;
int v[N][N], w[N][N];
int f[N];
int s[N];
int n, m;

int main() {
    cin >> n >>m ;
    for (int i = 1; i <= n; i++) {
        cin >> s[i];
        for (int j = 1; j <= s[i]; j++) {
            cin >> v[i][j] >> w[i][j];
        }
    }

    // 01背包+每组选一种
    /* 
       j逆序是因为      f[j] = max(f[j],        f[j - v[i][k]]        + w[i][k])
       等价变形是    f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i][k]] + w[i][k])
    */
    for (int i = 1; i <= n; i++) {
        for (int j = m; j >= 0; j--) {   //体积从大到小
            for (int k = 1; k <= s[i]; k++) {
                if (j >= v[i][k])  //这里需要判断一下 !!!!!因为我们j是到0
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
            }
        }
    }

    cout << f[m] << endl;

    return 0;
}

7.01背包问题求最优方案数

有N件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出 最优选法的方案数。注意答案可能很大,请输出答案模 10^9+7的结果。

解决方法:

用两个数组  f[i][v]意义同前述,g[i][v]表示这个子问题的最优方案的总数,则在求f[i][v]的同时求g[i][v]的伪代码如下: 

for i=1..N 
    for v=0..V 
          f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]} 
          if(f[i][v]==f[i-1][v]) 
                g[i][v]=g[i-1][v] 
           if(f[i][v]==f[i-1][v-c[i]]+w[i]) 
                g[i][v] += g[i-1][v-c[i]]   //因为可能两种方法都能达到最优 

tips:这里的初始化方法也有两种

  1. 所有的f【x】都初始化为0,所有的g【x】都初始化为1(表示啥物品都不能选,最大允许容量分别为0...m的最优方案数,自然都是1,啥都不放)
  2. f【0】初始为0,其余的f【x】初始为一个很大的负数(可以是INT_MIN),这样f【i】【j】就是恰用了j容量的时候最大获益,其实是为了f【i】【j】只能从f【0】【0】这样一步一步递推过来。 并且g【0】初始化为1,其余的g【x】初始化为0。这时候的g【x】表示容量恰是x的最优方案数

采用方案1初始化,最终答案就是g【m】,而采用方案2初始化最终的答案需要首先遍历f【i】,找到最大的值max_num,然后再遍历一遍f【i】,将等于max_num的f【i】的g【i】都加起来,就是最终的答案。

方案1代码:

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;

const int N= 1010,MOD=1e9+7;

//int f[N],cnt[N];
int v[N],w[N];

int main(){
    int n,m;
    cin>>n>>m;
  
    vector<int> f(N,0);
    vector<int> cnt(N,1); //初始啥都不能选,最大允许容量为0...m的最优方案数,自然都是1,啥都不放
  
    for(int i=1;i<=n;++i){
        cin>>v[i]>>w[i];
    }
    
    for(int i=1;i<=n;++i){
        for(int j=m;j>=v[i];--j){
            // 只选i-1的方案要比选了i的方案差
            if(f[j]<f[j-v[i]]+w[i])
                cnt[j]=cnt[j-v[i]];
            //也有可能两种方案最大获利一样,所以要加起来    
            else if(f[j]==f[j-v[i]]+w[i])
                cnt[j] += cnt[j-v[i]];
                
            f[j]=max(f[j],f[j-v[i]]+w[i]);
            cnt[j] %= MOD;
        }
    }
    
    cout<<cnt[m]<<endl;
    return 0;
    
}

方案二代码:

8.背包问题求具体方案

假如要求输出字典序最小的具体方案,那么需要逆序求商品

输出时的判断条件if(f[i][vol]==f[i+1][vol-v[i]]+w[i]);

vol每次输出之后都要减去v[i],所以判断的时候vol-v[i]有可能是负数导致数组越界,所以加一句vol-v[i]>=0的判断。

判断要不要选的逻辑是,当前能选就选(因为是字典序最小)

输出字典序代码:

#include<iostream>
#include<algorithm>
#include<cstdio>

using namespace std;

const int N=1010;

int n,m;
int v[N],w[N],f[N][N];

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>v[i]>>w[i];

    for(int i=n;i>=1;i--)   //商品 逆序求
        for(int j=0;j<=m;j++)
        {
            if(j>=v[i]) f[i][j]=max(f[i+1][j],f[i+1][j-v[i]]+w[i]);
            else f[i][j]=f[i+1][j];
        }

    int vol=m;
    for(int i=1;i<=n;i++)
    {
        
        if(vol<0)
        {
            break;
        }
        
        if(vol-v[i]>=0&&f[i][vol]==f[i+1][vol-v[i]]+w[i])
        {
            cout<<i<<' ';
            vol-=v[i];
        }

    }

    return 0;
}

 

9.有依赖的背包问题

问题描述:

有 N个物品和一个容量是 V 的背包。

物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。

如下图所示:
QQ图片20181018170337.png

如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。

每件物品的编号是 i,体积是 vi,价值是 wi,依赖的父节点编号是 pi。物品的下标范围是 1…N。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

f【i】【j】,选节点i,并且所用体积是j,以i为根的子树的最大收益是多少

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值