背包问题总结

背包问题总结

说到动态规划,背包问题是大家都绕不开的一个问题,这篇文章将从最简单的01背包问题开始,尽可能详细的给大家介绍各种背包问题的解法

01背包问题

首先,背包问题中最简单的问题也就是这个01背包问题,我们先一起来看一下这个问题

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

我们需要判断的是,对于每一个物品,是选择他更好,还是不选择他更好。不管是贪心还是枚举都不能很好的解决这个问题,这时候我们需要用动态规划的方式解决这个问题,

首先我们要设计出问题的状态f [i] [j]定义:前 i个物品,背包容量 j下的最优解(最大价值):

当前的状态依赖于之前的状态,可以理解为从初始状态f[0] [0] = 0开始决策,有 N 件物品,则需要 N 次决 策,每一次对第 i 件物品的决策,状态f[i] [j]不断由之前的状态更新而来。
1.当前背包容量不够(j < v[i]),没得选,因此前 i 个物品最优解即为前 i−1 个物品最优解这时候我们直接转移状态f[i] [j] = f[i - 1] [j]。
2.当前背包容量够,可以选,因此需要决策选与不选第 i 个物品:也就是要找的这两种情况的最优解。

选:f[i] [j] = f[i - 1] [j - v[i]] + w[i]。
不选:f[i] [j] = f[i - 1] [j] 。
我们的决策是如何取到最大价值,因此以上两种情况取 max() 。

为什么这个问题的结果可以成立,因为我们的每个状态都记录的是当前局部状态下的最优解,而每次更新新的物品时,我们一定是从之前的原始转态转移过来的,所以说不会影响后续结果的变化。话不多说,大家可以通过下面的代码来理解这个问题。

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int v[MAXN],w[MAXN];
int f[MAXN][MAXN];  // f[i][j], j体积下前i个物品的最大价值 
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 = 1; j <= m; j++)
        {
            //  当前背包容量装不进第i个物品,则价值等于前i-1个物品
            if(j < v[i]) 
                f[i][j] = f[i - 1][j];
            // 能装,需进行决策是否选择第i个物品
            else    
                f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
        }           
    cout << f[n][m] << endl;
    return 0;
}

代码做的其实很简单,就是对于每个物品,都把所有体积枚举一遍,保证所有情况都会被考虑到,不重不漏。接下来给大家介绍一个关于优化01背包问题空间复杂度的方法。

其实原理很简单,就是每次的f[I] [J]的值都是从i-1转移过来的,所以我们可以定义f时直接忽略第一维,然后倒序枚举所有用到的体积

二维情况下,状态f[i][j]是由上一轮i - 1的状态得来的,f[i] [j]与f[i - 1] [j]是独立的。而优化到一维后,如果我们还是正序,则有f[较小体积]更新到f[较大体积],则有可能本应该用第i-1轮的状态却用的是第i轮的状态。如果采用倒序的方法,我们每次更新的j都是用上一维的j直接进行转移的,由于枚举体积是倒序进行,不会导致用到被覆盖过的结果,可以节省一些空间复杂度

具体写法就是

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int f[MAXN];  
int main() 
{
    int n, m,v,w;   
    cin >> n >> m;
    for(int i = 1; i <= n; i++) {
        cin >> v >> w;      // 边输入边处理,不需要提前储存
        for(int j = m; j >= v; j--)
            f[j] = max(f[j], f[j - v] + w);
    }
    cout << f[m] << endl;
    return 0;
}

完全背包问题

说完了01背包,接下来给大家介绍的是完全背包问题,因为大部分背包问题基本原理都大致相同,所以之后所有问题,都会讲他变化的地方,

先来看一下完全背包和01背包的区别

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

其实区别就是,每个物品的数量不做限制了,我们先和刚刚一样,用最朴素的写法尝试理解这个问题

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

实际上,我们只是对于每个体积多了一次枚举选择几个该物品的过程,实际上,我们也可以吧问题理解成,选一个第i个物品是一个状态,选2个是一个状态,…这样问题就转化成了01背包问题。理解了之后,我们考虑如何优化这个问题。

对于这个问题,其实我们理解f [i] [j]=max(f[i,j-v]+w , f[i-1] [ j]) 也就是说,在体积从小到大枚举的过程中,我们可以记录已经选了一个该物品的结果,来考虑需不需要考虑是否选择第二个物品。不需要第三层循环来限制选择的数量每个体积都会被上次该体积的结果直接更新,也就是下面这样。

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

和刚刚的01背包基本一样,唯一的区别就是这里max里的第二项下标从i-1变成了i。也就是说,我们直接在这一维的基础上继续考虑是否继续选择这个物品,在优化一下空间

 for(int i = 1 ; i<=n ;i++)
    for(int j = v[i] ; j<=m ;j++)//注意了,这里的j是从小到大枚举,因为我们再次选择不会覆盖结果,所有状态就是正常转移过来的
            f[j] = max(f[j],f[j-v[i]]+w[i]);

多重背包

有 N 种物品和一个容量是 V 的背包。第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。

最朴素的想法就是,把问题拆成01背包问题来解决

#include <bits/stdc++.h>
using namespace std;
int a[10005],b[10005];
int main()
{
    int t=0,n,m,dp[10005]={ },w,v,s;
    cin>>n>>m;
    while(n--)
    {
    cin>>v>>w>>s;
    while(s--)
    {a[++t]=v;
    b[t]=w;}//把多重背包拆成01背包
    }
    for(int i=1;i<=t;i++)
    for(int j=m;j>=a[i];j--)
    dp[j]=max(dp[j-a[i]]+b[i],dp[j]);
    cout<<dp[m]<<endl;
    return 0;
}

简单点写的话

int dp[1005],n,t,v,w,s;
    cin>>n>>t;
    while(n--)
    {
    cin>>w>>v>>s;
    for(int i=1;i<=s;i++)
    for(int j=t;j>=w;j--)
    dp[j]=max(dp[j],dp[j-w]+v);
    }

不过,这种方法能解决问题的范围是相当有限的,如果想要解决更大规模的问题,我们需要进一步优化

我们令 dp[j] 表示容量为j的情况下,获得的最大价值
那么,针对每一类物品 i ,我们都更新一下 dp[m] --> dp[0] 的值,最后 dp[m] 就是一个全局最优值
显而易见,m 一定等于 k * v + j,其中 0 <= j < v
所以,我们可以把 dp 数组分成 j 个类,每一类中的值,都是在同类之间转换得到的
也就是说,dp[k * v+j] 只依赖于 { dp[j], dp[v+j], dp[2v+j], dp[3v+j], … , dp[kv+j] }

因为我们需要的是{ dp[j], dp[v+j], dp[2v+j], dp[3*v+j], … , dp[k * v+j] } 中的最大值,
可以通过维护一个单调队列来得到结果。这样的话,问题就变成了 j 个单调队列的问题、

层层递进,维护v次单调队列,每次都更新每个体积下的最优解,最后剩下的记过就是最优解。

#include<bits/stdc++.h>
using namespace std;
const int N = 20010;
int dp[N], pre[N], q[N];
int n, m;
int main() {
    cin >> n >> m;
    for (int i = 0; i < n; ++i) {
        memcpy(pre, dp, sizeof(dp));
        int v, w, s;
        cin >> v >> w >> s;
        for (int j = 0; j < v; ++j) {
            int head = 0, tail = -1;
            for (int k = j; k <= m; k += v) {
                if (head <= tail && k - s*v > q[head])
                    ++head;
                while (head <= tail && pre[q[tail]] - (q[tail] - j)/v * w <= pre[k] - (k - j)/v * w)
                    --tail;
                if (head <= tail)
                    dp[k] = max(dp[k], pre[q[head]] + (k - q[head])/v * w);
                q[++tail] = k;
            }
        }
    }
    cout << dp[m] << endl;
    return 0;
}

二维费用背包问题

有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。输出最大价值。

问题和01背包基本一致,只是需要多加一维判断体积维度的。倒着一起枚举就行。

#include<bits/stdc++.h>
using namespace std;
const int maxn=1010;
int f[maxn][maxn];
int main()
{
    int n,v,m,V,M,W;
    cin>>n>>v>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>V>>M>>W;
        for(int j=v;j>=V;j--)
        for(int k=m;k>=M;k--)
        f[j][k]=max(f[j][k],f[j-V][k-M]+W);
    }
    cout<<f[v][m]<<endl;
    return 0;
}

分组背包

有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。输出最大价值。

分组背包要求,每组物品只有一个物品可以最后被使用,为了达到这个目的,我们就像01背包一样,每一种都选择一遍,实际上也可以理解成多重背包,就是选1个是一种方案,选2个也是一种方案,最后都看做01背包来解决这个问题。

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n, m;
int v[N][N], w[N][N], s[N];
int f[N];
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ )
    {
        cin >> s[i];
        for (int j = 0; j < s[i]; j ++ )
            cin >> v[i][j] >> w[i][j];
    }
    for (int i = 1; i <= n; i ++ )
        for (int j = m; j >= 0; j -- )
            for (int k = 0; k < s[i]; k ++ )
                if (v[i][k] <= j)
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
    cout << f[m] << endl;
    return 0;
}

有依赖的背包问题

在讲有依赖的背包问题之前,我们前介绍一个这类题目的前身,也是2006年NOIP提高组的一个题目

金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。
更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过N元钱就行”。
今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,
如果要买归类为附件的物品,必须先买该附件所属的主件。
每个主件可以有0个、1个或2个附件。
附件不再有从属于自己的附件。
金明想买的东西很多,肯定会超过妈妈限定的N元。
于是,他把每件物品规定了一个重要度,分为5等:用整数1~5表示,第5等最重要。
他还从因网上查到了每件物品的价格(都是10元的整数倍)。
他希望在不超过N元(可以等于N元)的前提下,使每件物品的价格与重要度的乘积的总和最大。
设第j件物品的价格为v[j],重要度为w[j],共选中了k件物品,编号依次为j1,j2,…,jk,则所求的总和为:
v[j1]∗w[j1]+v[j2]∗w[j2]+…+v[jk]∗w[jk](其中*为乘号)
请你帮助金明设计一个满足要求的购物单。
 输入格式
输入文件的第1行,为两个正整数,用一个空格隔开:N m,其中N表示总钱数,m为希望购买物品的个数。
从第2行到第m+1行,第j行给出了编号为j-1的物品的基本数据,每行有3个非负整数v p q,其中v表示该物品的价格,p表示该物品的重要度(1~5),q表示该物品是主件还是件。
如果q=0,表示该物品为主件,如果q>0,表示该物品为附件,q是所属主件的编号。
输出格式
输出文件只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(<200000)。
数据范围
N<32000,m<60,v<10000
输入样例:
1000 5
800 2 0
400 5 1
300 5 1
400 3 0
500 2 0
输出样例:
2200

题意就是,给你一些物品组合,必须买了主件才能购买附件,实际上,这不是严格的有依赖背包问题,但有依赖背包问题是在这个问题之后被提出的。所以我们先来看一下这个问题,实际上,观察后可以发现,这道题其实就是一种分组背包,举个例子,如果有1个主件,2个附件对于每一个主件,我们看成一组,这组物品中,有全都不选,只选主件,选主件和附件1,选主件和附件2,全都选5种情况共同组成。我们只需要按照分组背包的方式来枚举所有情况即可,为了方便统计所有情况,我们可以用二进制枚举的技巧,很简单就能枚举出所有情况。有一些细节可以再代码找到对应的体现。

#include <bits/stdc++.h>
#define v first
#define w second
using namespace std;
typedef pair<int, int> PII;
const int N = 60, M = 32010;
int n, m;
PII master[N];
vector<PII> servent[N];
int f[M];
int main()
{
    cin >> m >> n;
    for (int i = 1; i <= n; i ++ )
    {
        int v, p, q;
        cin >> v >> p >> q;
        p *= v;//p是价值
        if (!q) master[i] = {v, p};//是主件就直接加进来
        else servent[q].push_back({v, p});
    }
    for (int i = 1; i <= n; i ++ )//按顺序枚举
        for (int u = m; u >= 0; u -- )//倒着更新可用体积,相当于分组背包,只不过每组分为选不选的问题。
        {
            for (int j = 0; j < 1 << servent[i].size(); j ++ )//二进制枚举所有组内的选择
            {
                int v = master[i].v, w = master[i].w;//首先考虑只选主件
                for (int k = 0; k < servent[i].size(); k ++ )
                    if (j >> k & 1)//根据情况确定选不选某个附件
                    {
                        v += servent[i][k].v;
                        w += servent[i][k].w;
                    }
                if (u >= v) f[u] = max(f[u], f[u - v] + w);
            }
    }
    cout << f[m] << endl;
    return 0;
}
有 N 个物品和一个容量是 V 的背包。
物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。
如下图所示:
		1
	   / \
	  2   3
	 /\
    4  5
如果选择物品5,则必须选择物品1和2。这是因为2是5的父节点,1是2的父节点。
每件物品的编号是 i,体积是 vi,价值是 wi,依赖的父节点编号是 pi。物品的下标范围是 1…N。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数 N,V,用空格隔开,分别表示物品个数和背包容量。
接下来有 N 行数据,每行数据表示一个物品。
第 i 行有三个整数 vi,wi,pi,用空格隔开,分别表示物品的体积、价值和依赖的物品编号。
如果 pi=−1,表示根节点。 数据保证所有物品构成一棵树。
输出格式
输出一个整数,表示最大价值。
数据范围
1≤N,V≤100
1≤vi,wi≤100
父节点编号范围:
内部结点:1≤pi≤N;
根节点 pi=−1;
输入样例
5 7
2 3 -1
2 2 1
3 5 1
4 7 2
3 6 2
输出样例:
11

其实大体思路还是按照分组背包来选择,需要结合树形dp来一起完成。对于每个子树,根节点是一定要选的,对于子树中的内容,我们就按照分组背包来枚举所有的选法组合,找到可获得体积的最大值。区别是子树的节点可能会过多,会导致搜索深度过大,我们只能按照体积划分所有子树的可能选择方式。给每个子树分配0到总体积减根体积的体积,来看给定体积后,可以选到的最大值。

#include<iostream>
#include<vector>
using namespace std;
int f[110][110];//f[x][v]表达选择以x为子树的物品,在容量不超过v时所获得的最大价值
vector<int> g[110];
int v[110],w[110];
int n,m,root;

int dfs(int x)
{
    for(int i=v[x];i<=m;i++) f[x][i]=w[x];//点x必须选,所以初始化f[x][v[x] ~ m]= w[x]
    for(int i=0;i<g[x].size();i++)
    {
        int y=g[x][i];
        dfs(y);
        for(int j=m;j>=v[x];j--)//j的范围为v[x]~m, 小于v[x]无法选择以x为子树的物品
            for(int k=0;k<=j-v[x];k++)//分给子树y的空间不能大于j-v[x],不然都无法选根物品x
                f[x][j]=max(f[x][j],f[x][j-k]+f[y][k]);
    }
}

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        int fa;
        cin>>v[i]>>w[i]>>fa;
        if(fa==-1)
            root=i;
        else
            g[fa].push_back(i);
    }
    dfs(root);
    cout<<f[root][m];
    return 0;
}

写在最后

最后,背包问题的变化还有很多,本文也不可能全部覆盖,希望他可以帮助你对背包问题有一点新的理解,如果浪费了你的时间,那就先说声抱歉,也希望你能早日理解背包问题,如果有什么地方错误,欢迎评论或私信指出,谢谢大家

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值