背包问题汇总(八讲)

01背包

原题链接: https://www.acwing.com/problem/content/2/

题意

有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]}
空间优化:可以注意到当前状态完全依赖上一次状态,因此可以优化成一维。看代码。

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

using namespace std;

const int N = 1010;
//int f[N][N];
int f[N];
int v[N], w[N];
int n, m;

//无空间优化版
/*int main01()
{
    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 = 0; j <= m; j++)
        {
            f[i][j] = f[i - 1][j];//表示选第i件物品物品
            if(j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]); //表示不选第i将物品
        }
    }
    
    cout << f[n][m] << endl;
  
    return 0;
}*/

//空间优化版
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 = m; j >= v[i]; j--) //注意内层循环必须从大到小,原因是我们状态更新的时候要用上一层的f[j], 如果从小到大枚举的话,使用的f[j]一定是当前层。这就不正确了。
        {
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    
    cout << f[m] << endl;
    
    return 0;
}


两种问法

  1. 有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。并且不超过V.-----上面题解。 如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0…V]全部设为0.
  2. 有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。并且恰好等于V.-----那么在初始化时除了f[0]为0其它f[1…V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。
  3. 可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。
    这个小技巧完全可以推广到其它类型的背包问题,后面也就不再对进行状态转移之前的初始化进行讲解。

完全背包

题意

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

题解

这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。如果仍然按照解01背包时的思路,令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:
f[i][v]=max{f[i-1][v-kc[i]]+kw[i]|0<=k*c[i]<=v}
空间优化:
由于当前层不止可以选一件,因此状态方程依赖当前层。内层循环从小到大。
你会发现,这个代码与01背包的代码只有v的循环次序不同而已。为什么这样一改就可行呢?首先想想为什么01背包中要按照v=V…0的逆序来循环。这是因为要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-c[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果f[i-1][v-c[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果f[i][v-c[i]],所以就可以并且必须采用v=0…V的顺序循环。这就是这个简单的程序为何成立的道理。

#include "iostream"
using namespace std;

//int f[1010];
int f[1010][1010];
int w[1010],v[1010];

int main(){
    
    int n,m;
    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 = 0; j <= m; j++)
        {
            for(int k = 0; k * v[i] <= j; k++)
            {
                f[i][j] = max(f[i][j] , f[i - 1][j - k * v[i]] + k * w[i]);
            }
        }
    }
    
    cout << f[n][m] << endl;
    
    //空间优化:

   /* 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;
}

多重背包

题意

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

题解

1.一般解法:转化成01背包,将n[i]个物品拆分成,这样就增加了物品地数量,每一个物品有选和不选两种选择。
2. 二进制优化:思路同样是拆分成01背包。但是拆分的方式和解法一不同,这里先明白一个性质:任何数都可以用2^0, 2^1,…来表示, 这样可以将n[i] 拆分成若干2的幂加上另一个数。
例如:13 = 1 + 2 + 4 + 6,这样就可以从解法一的13个物品优化成只有4个物品,价值重量分别是:{1 * v[i], 1 * w[i]}, {2* v[i], 2 * w[i]},{4 * v[i], 4 * w[i]},{6 * v[i], 6* w[i]}.
3. 单调队列优化。超出能力范围。。。。可参考楼天成的《男人八题》

一般解法

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

using namespace std;

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

int main()
{
    int n, m;
    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 = 0; 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;
}


二进制优化

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

using namespace std;

const int N = 1010;
int f[N], v[N], w[N], s[N];


struct Good
{
    int v, w;
};

vector<Good> goods;

int main()
{
    int n, m;

    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 k = 1; k <= s[i]; k *= 2)
        {
            s[i] -= k;
            goods.push_back({k * v[i], k * w[i]});
        }
        
        if(s[i] > 0) goods.push_back({s[i] * v[i], s[i] * w[i]});
    }
    
    //01背包模板
    for(auto good : goods) //变成了在goods里的操作了,物品数量,价值,发生了变化。
    {
        for(int j = m; j >= good.v; j--)
        {
            f[j] = max(f[j], f[j - good.v] + good.w);
        }
    }
    
    cout << f[m] << endl;
    
    return 0;
}

混合背包

题意

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

题解

考虑到在01背包和完全背包中给出的代码只有一处不同,故如果只有两类物品:一类物品只能取一次,另一类物品可以取无限次,那么只需在对每个物品应用转移方程时,根据物品的类别选用顺序或逆序的循环即可。
考虑多重背包问题可以二进制优化成01背包问,因此,只需要求01和完全背包组合就可以了。

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

using namespace std;

const int N = 1010;
int f[N];

struct Thing
{
    int kind, v, w;
};

int main()
{
    int n, m;
    vector<Thing> things;
    cin >> n >> m;
    
    for(int i = 1; i <= n; i++)
    {
        int v, w, s;
        cin >> v >> w >> s;
        if(s == -1) things.push_back({-1, v, w});
        if(s == 0) things.push_back({0, v, w});
        if(s > 0)
        {
            for(int k = 1; k <= s; k *= 2 )
            {
                s -= k;
                things.push_back({-1, k * v, k * w});
            }
            if(s > 0) things.push_back({-1, s * v, s * w});
        }
    }
    
    for(auto thing : things)
    {
        if(thing.kind == -1)
        {
            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;
    
}

二维费用背包

题意

  1. 二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为w[i]。
  2. 题目: 有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。
    每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。
    求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
    输出最大价值。

题解

费用加了一维,只需状态也加一维即可。设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]}
如前述方法,可以只使用二维的数组:当每件物品只可以取一次时变量v和u采用逆序的循环,当物品有如完全背包问题时采用顺序的循环。当物品有如多重背包问题时拆分物品.变成01背包。
2. 物品总个数的限制
有时,“二维费用”的条件是以这样一种隐含的方式给出的:最多只能取M件物品。这事实上相当于每件物品多了一种“件数”的费用,每个物品的件数费用均为1,可以付出的最大件数费用为M。换句话说,设f[v][m]表示付出费用v、最多选m件时可得到的最大价值,则根据物品的类型(01、完全、多重)用不同的方法循环更新,最后在f[0…V][0…M]范围内寻找答案。

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

using namespace std;

const int N = 110;
int f[N][N];

int main()
{
    int n, m, p;
    cin >> n >> m >> p;
    for(int i = 1; i <= n; i++)
    {
        int v, w, value;
        cin >> v >> w >> value;
        for(int j = m; j >= v; j--)
        {
            for(int k = p; k >= w; k--)
            {
                f[j][k] = max(f[j][k], f[j - v][k - w] + value);
            }
        }
    }
    
    cout << f[m][p] << endl;
    
    return 0;
}

分组背包

题意

有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
forv=V…0
for 所有的i属于组k
f[v]=max{f[v],f[v-c[i]]+w[i]}

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

using namespace std;

const int N = 110;
int f[N]; //表示前k组物品花费费用v能取得的最大权值
int w[N], v[N];

int main()
{
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int s;
        cin >> s;
        for(int j = 0; j < s; j++) cin >> v[j] >> w[j];
        
        for(int j = m; j >= 0; j--)
        {
            for(int k = 0; k < s; k++)
            {
              
                if(j >= v[k]) f[j] = max(f[j], f[j - v[k]] + w[k]);
            }
        }
    }
    
    cout << f[m] << endl;
    
    return 0;
}


背包求方案数

题意

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出 最优选法的方案数。注意答案可能很大,请输出答案模 109+7 的结果。

题解

一般而言,背包问题是要求一个最优值,如果要求输出这个最优值的方案,可以参照一般动态规划问题输出方案的方法:记录下每个状态的最优值是由状态转移方程的哪一项推出来的,换句话说,记录下它是由哪一个策略推出来的。便可根据这条策略找到上一个状态,从上一个状态接着向前推即可。

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

using namespace std;

const int N = 1010, INF = 0x3f3f3f3f, mod = 1e9 + 7;
int f[N], g[N]; //f[j] 表示体积恰好是j的情况下最大价值,g[j]表示体积恰好是j的情况下最大方案数

int main()
{
    //初始化 
    g[0] = 1;
    memset(f, -INF, sizeof f);
    f[0] = 0;
    
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int v, w;
        cin >> v >> w;
        for(int j = m; j >= v; j--)
        {
            int t = max(f[j], f[j - v] + w); //记录t是从哪个状态转移来的
            int s = 0; //s表示当前状态的方案,它由它的前面的状态相加而来
            if(t == f[j]) s += g[j];
            if(t == f[j - v] + w) s += g[j - v];
            if(s > mod) s -= mod;
            
            f[j] = t;
            g[j] = s;
        }
    }
    
    //找到最大价值
    int mmax = 0;
    for(int i = 0; i <= m; i++) mmax = max(mmax, f[i]);
    
    //统计最大价值的方案数
    int res = 0;
    for(int i = 0; i <= m; i++)
    {
        if(f[i] == mmax) res += g[i];
        if(res > mod) res -= mod;
    }
    
    cout << res << endl;
    
    return 0;
}

背包求具体方案

题意

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出 字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 1…N。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

题解

题目要求输出字典序最小的解,假设存在一个包含第1个物品的最优解,为了确保字典序最小那么我们必然要选第一个。那么问题就转化成从2~N这些物品中找到最优解。之前的f(i,j)f(i,j)记录的都是前ii个物品总容量为jj的最优解,那么我们现在将f(i,j)f(i,j)定义为从第ii个元素到最后一个元素总容量为jj的最优解。接下来考虑状态转移:
f(i,j)=max(f(i+1,j),f(i+1,j−v[i])+w[i])f(i,j)=max(f(i+1,j),f(i+1,j−v[i])+w[i])
两种情况,第一种是不选第ii个物品,那么最优解等同于从第i+1i+1个物品到最后一个元素总容量为jj的最优解;第二种是选了第ii个物品,那么最优解等于当前物品的价值w[i]w[i]加上从第i+1i+1个物品到最后一个元素总容量为j−v[i]j−v[i]的最优解。
计算完状态表示后,考虑如何的到最小字典序的解。首先f(1,m)f(1,m)肯定是最大价值,那么我们便开始考虑能否选取第1个物品呢。
如果f(1,m)=f(2,m−v[1])+w[1]f(1,m)=f(2,m−v[1])+w[1],说明选取了第1个物品可以得到最优解。
如果f(1,m)=f(2,m)f(1,m)=f(2,m),说明不选取第一个物品才能得到最优解。
如果f(1,m)=f(2,m)=f(2,m−v[1])+w[1]f(1,m)=f(2,m)=f(2,m−v[1])+w[1],说明选不选都可以得到最优解,但是为了考虑字典序最小,我们也需要选取该物品。

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

using namespace std;

const int N = 1010;
int f[N][N];
int v[N], w[N];

int main()
{
    int n, m;
    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++)
        {
            f[i][j] = f[i + 1][j];
            if(j >= v[i]) f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]);
        }
    
    }
    
     int cur_v = m;
    for(int i = 1 ; i <= n ; i++)
    {   //如果是最后一个元素,特判一下,防止越界即可
        if(i == n && cur_v >= v[i])
        {
            printf("%d ",i);
            break;
        }
        if(cur_v <= 0)
            break;
        //判断下标是否越界
        if(cur_v - v[i]>=0 && f[i][cur_v] == f[i + 1][cur_v - v[i]] + w[i]){
            printf("%d ",i);
            cur_v = cur_v - v[i];//选了第i个物品,剩余容量就要减小。
        }
    }

    
    return 0;
}


参考

https://www.acwing.com/solution/content/2687/

https://www.acwing.com/file_system/file/content/whole/index/content/164826/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值