背包问题分析

鸣谢:@程墨竹

1 背包问题的概念

给定几件(组)物品,每种物品都有自己的代价收益,在最大可承受的代价内,我们如何选择,才能使得获得的总收益最高。
它可以看作是一种动态规划问题

2 背包问题的分类和解答

2.1 01背包

2.1.1 分析

01就是指物品只有拿和不拿两种选择,
正因如此
所以在Bag[i][j] 表示在有 i 种物品,容量为 j 的背包 中的状态只有
选择不拿

	Bag[i-1][j]
	/*
	忽略第i个物品->不拿
	*/

或者拿

	 Bag[i-1][j-v[i]]+c[i]
	 /*
	 Bag[以前几个物品][剩余空间]的最优解加上当前物品的价值->拿
	 */

则状态转移方程就是以上两者的最大值

	Bag[i][j]=max(Bag[i-1][j],Bag[i-1][j-v[i]]+c[i]);
	//拿与不拿的价值哪个大?

2.1.2 主要代码

上面说到,物品只有拿与不拿两种状态,但在拿的时候有一个需要注意的点就是确保空间足够

	for(int i=1;i<=n;i++)
        for(int j=V;j>=0;j--)
        //可不可以拿
        	if(j>=v[i])//拿得了
        		Bag[i][j]=max(Bag[i-1][j]/*不拿*/,Bag[i-1][j-v[i]]+c[i]/*拿*/);
            else//拿不了
            	Bag[i][j]=Bag[i-1][j];

2.1.3 完整代码

题目描述:
给定背包的容量和物品数量,以及每个物品的体积和价值,求可得的最大价值
样例输入:
10 4
2 1
3 3
4 5
7 9
样例输出:
12
数据范围:
所有数据和输出都在1000范围之内
参考代码:

#include <iostream>
using namespace std;
int main() {
    
    
    int V,n;
    cin>>V>>n;
    int v[n+1],c[n+1];
    int Bag[n+1][V+1];
    for(int i=1;i<=n;i++)
        cin>>v[i]>>c[i];
    for(int i=0;i<=V;i++)
        Bag[0][i]=0;
    for(int i=1;i<=n;i++)
        for(int j=V;j>=0;j--)
        	if(j>=v[i])
                Bag[i][j]=max(Bag[i-1][j],Bag[i-1][j-v[i]]+c[i]);
            else
                Bag[i][j]=Bag[i-1][j];
    cout<<Bag[n][V];
    return 0;
}

2.1.4 优化

生活中呢,有很多
很多时候,二维数组实在是占空间。
怎么办呢?
我们回顾一下01背包的状态转移方程:

Bag[i][j]=max(Bag[i-1][j],Bag[i-1][j-v[i]]+c[i])

发现什么了吗?
i除了指向当前物品
仿佛没有作用
那么,我们可以将Bag[n][V]改成Bag[V],
大概修改一下就是:

Bag[j]=max(Bag[j],Bag[j-v[i]+c[i]])

那么直接上完整代码:

#include <iostream>
using namespace std;
int main() {
    
    
    int V,n;
    cin>>V>>n;
    int v[n+1],c[n+1];
    int Bag[V+1];
    for (int i=1;i<=n;i++)
        cin>>v[i]>>c[i];
    for (int i=0;i<=V;i++)
        Bag[i]=0;
    for (int i=1;i<=n;i++)
        for (int j=V;j>=v[i];j--)
        //之所以这里不加if,是因为如果空间不够就更新不了,因此再往下也没有意义
            Bag[j]=max(Bag[j],Bag[j-v[i]]+c[i]);
    cout<<Bag[V];
    return 0;
}

2.1.5 总结

01背包是所有背包问题基础,请大家一定要掌握
也一定要掌握状态转移方程:

Bag[j]=max(Bag[j],Bag[j-v[i]]+c[i])

2.2 完全背包

2.2.1 分析

完全背包问题中的每种物品都有无数件,可以无限取拿
我们这时就可以开 新循环k( 0 ~ j ( 1 ~ V ) / v[ 0 ~ n ] ) 来遍历这个物品可取拿的数量
根据01背包的状态转移方程:

	Bag[i][j]=max(Bag[i-1][j],Bag[i-1][j-v[i]]+c[i])
	//原方程

来推出完全背包的状态转移方程:

	Bag[i][j]=max(Bag[i-1][j],Bag[i-1][j-v[i]*k]+c[i]*k)
	//k是当前物品拿取的数量

2.2.2 主要代码

因为要算出物品可取的数量,j只有从1到V循环答案才正确

	for(int i=1;i<=n;i++)
        for(int j=1;j<=V;j++)
        	for(int k=0;k<=j/v[i];k++)
        	// j/v[i] 就是物品可取数量
        		if(j>=v[i]*k)
        		//可以装不?
	                Bag[i][j]=max(Bag[i-1][j],Bag[i-1][j-v[i]*k]+c[i]*k);
	        	/*
	        	因为若装不了多的就装少一点(不管k怎么变化,都是赋值给Bag[i][j])
	        	总之要尽量往大了装
	        	*/

2.2.3 完整代码

题目描述:
给定背包的容量和物品数量,以及每种物品的体积和价值,每种物品有无数个,求可得的最大价值
样例输入:
10 4
2 1
3 3
4 5
7 9
样例输出:
12
数据范围:
所有数据和输出都在1000范围之内
参考代码:

#include <iostream>
using namespace std;
int main() {
    
    
//背包容量和物品总数
    int V,n;
    cin>>V>>n;
//物品体积与价值
    int v[n+1],c[n+1];
    int Bag[n+1][V+1];
    for(int i=1;i<=n;i++)
        cin>>v[i]>>c[i];
    for(int i=0;i<=V;i++)
        Bag[0][i]=0;
    for(int i=0;i<=n;i++)
    	Bag[i][0]=0;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=V;j++)
        	for(int k=0;k<=j/v[i];k++)
        		if(j>=v[i]*k)
	                Bag[i][j]=max(Bag[i-1][j],Bag[i-1][j-v[i]*k]+c[i]*k);
    cout<<Bag[n][V];
    return 0;
}

2.2.4 简化

因为以下代码所求的是

for(int k=0;k<=j/v[i];k++)
	if(j>=v[i]*k)
		Bag[i][j]=max(Bag[i-1][j],Bag[i-1][j-v[i]*k]+c[i]*k);

Bag[i-1][j] Bag[i-1][j-v[i]]+c[i]
Bag[i-1][j-v[i]×2]+c[i]×2
……
Bag[i-1][j-v[i]×j/v[i]]+c[i]×j/v[i]

这些项的最大值
但是我们将

{ Bag[i-1][j-v[i]]+c[i]
Bag[i-1][j-v[i]×2]+c[i
……
Bag[i-1][j-v[i]×j/v[i]]+c[i]×j/v[i] }

这几项做一个简化:

{ Bag[i-1][j-v[i]],Bag[i-1][j-v[i]×2]+c[i],……,Bag[i-1][j-v[i]×k]+(k-1)×c[i]}+c[i]

令以上几项的最大值为X
则状态转移方程是

Bag[i][j]=max(Bag[i-1][j],X+c[i])

{ Bag[i-1][j-v[i]],Bag[i-1][j-v[i]×2]+c[i],……,Bag[i-1][j-v[i]×k]+(k-1)×c[i]}+c[i]

中,
我们将“j-v[i]”设为Y,

{ Bag[i-1][Y],Bag[i-1][Y-v[i]]+c[i],……,Bag[i-1][Y-v[i]×(k-1)]+(k-1)×c[i]}+c[i]

再次简化得

{ Bag[i-1][Y],Bag[i-1][Y-v[i]]+c[i],……,Bag[i-1][Y-v[i]×k]+k*c[i]}+c[i]

明显Bag[i][Y]=max(Bag[i−1][(Y−k∗w[i]]+k∗v[i]))
所以其实在一直重复计算Bag[i][Y]
放入i之前应该是
Bag[i][Y]
此刻我们并不知道这个状态放入的i的数量
则我们在Bag[i][j]预留i的位置
现在的状态转移方程是

Bag[i][j]=max(Bag[i-1][j],Bag[i][j-v[i]]+c[i])

注意这里的状态转移方程虽然和01背包问题的状态转移方程相似但所代表的意义不同
我们再优化一下数组:

Bag[j]=max(Bag[j],Bag[j-v[i]]+c[i]);

for(int i=1;i<=n;i++)
	for (int j=v[i];j<=V;j++)
            Bag[j]=max(Bag[j],Bag[j-v[i]]+c[i]);

2.3 多重背包

2.3.1 分析

多重背包问题中的物品有指定数量多个,还是开新循环k(0 ~ n[i])来遍历这个物品拿取的数量,即:

	Bag[j]=max(Bag[j],Bag[j-v[i]*k]+c[i]*k);

2.3.2 基础代码

我们按上述方法,即开新循环遍历拿取数量来做

#include <iostream>
using namespace std;
int main()
{
    
    
    int V,N;
    cin>>V>>N;
    int v[N+1],c[N+1],n[N+1];
    int Bag[V+1];
    for (int i=1;i<=N;i++)
        cin>>v[i]>>c[i]>>n[i];
    for (int i=0;i<=V;i++)
        Bag[i]=0;
	for (int i=1;i<=N;i++)
        for (int j=V;j>=v[i];j--)
        //不用算k循环的次数,所以还是从V到v[i]
            for(int k=0;k<=n[i];k++)
        	//遍历每种物品的数量所可得的最大价值
                if(j>=k*v[i])
                    Bag[j]=max(Bag[j],Bag[j-k*v[i]]+k*c[i]);
    cout<<Bag[V];
    return 0;
}

2.3.3 简化思路

以上思路的时间复杂度是:O(V×∑n[i])
但是还有更快的方法求解,它的时间复杂度是:O(V×∑㏒ n[i])
我们把 第i种物品换成n[i]件 01背包中的物品
那么,怎么 取0…n[i]件 才能等价于取若干件代换以后的物品并且取超过n[i]件的数量的策略不能出现
方法是:将第i(1~N)种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数
使这些系数分别为1,2,4,…,2(k-1),n[i]-2k+1, *1
例如,如果n[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品

数量 组合方式
1 1
2 2
3 1+2
4 4
5 1+4
6 6
7 1+6
8 2+6
9 1+2+6
10 4+6
11 1+4+6
12 2+4+6
13 1+2+4+6
14 无法

综上所述,伪代码如下:

N<-0
△在输入时就调整:
for i <-1 to tmpn i++ do
    tmp <- 1
    while n>=tmp do
     	N<-N+1
        v[N] <- x*tmp
        c[N] <- y*tmp
        n <- n-tmp
        △如上所述的原理
        tmp <- tmp*2
    end
    N <- N+1
    v[N] <- x*n
    c[N] <- y*n
end

2.3.4 完整代码

题目描述:
给定背包的容量和物品数量,以及每种物品的体积和价值以及数量,求可得的最大价值
样例输入:
8 2
2 1 4
4 1 2
样例输出:
4
数据范围:
所有数据和输出都在1000范围之内
参考代码:

#include <iostream>
#define maxn 12345
using namespace std;
int main()
{
    
    
    int V,tmpn,N=0;
    cin>>V>>tmpn;
    int x,y,n,tmp;
    int v[maxn],c[maxn];
    int Bag[V+1];
    for (int i=1;i<=tmpn;i++) {
    
    
        cin>>x>>y>>n;
        tmp=1;
        while(n>=tmp) {
    
    
            v[++N]=x*tmp;
            c[N]=y*tmp;
            n-=tmp;
            tmp*=2;
        }
        v[++N]=x*n;
        c[N]=y*n;
    }
    for (int i=0;i<=V;i++)
        Bag[i]=0;
    for (int i=1;i<=N;i++)
        for (int j=V;j>=v[i];j--)
                    Bag[j]=max(Bag[j],Bag[j-v[i]]+c[i]);
    cout<<Bag[V];
    return 0;

2.4 三种背包的混合情况

2.4.1 分析

这个题顾名思义,是将01,完全,多重背包三种问题混合起来,
主要代码的伪代码如下:

for i <- 1 to N i++
	if n[i]==0 then
		for j <- v[i] to V j++
		△完全背包,j正着跑
			Bag[j] <- max(Bag[j],Bag[j-v[i]]+c[i])
		end
	else
		for j <- V to v[i] j--01和多重背包,j反着跑
	        for k <- 0 to n[i] k++
	            if j>=k*v[i] then
	                Bag[j] <- max(Bag[j],Bag[j-k*v[i]]+k*c[i])
	        end
	    end
end

2.4.2 完整代码

题目描述:
给定背包的容量和物品数量,以及每种物品的体积和价值以及个数,0为无数个,求可得的最大价值
样例输入:
10 3
2 1 0
3 3 1
4 5 4
样例输出:
11
数据范围:
所有数据和输出都在1000范围之内
参考代码:

#include <iostream>
using namespace std;
int main()
{
    
    
    int V,N;
    cin>>V>>N;
    int v[N+1],c[N+1],n[N+1];
    int Bag[V+1];
    for (int i=1;i<=N;i++)
        cin>>v[i]>>c[i]>>n[i];
    for (int i=0;i<=V;i++)
        Bag[i]=0;
	for (int i=1;i<=N;i++)
		if(n[i]==0) {
    
    
			for (int j=v[i];j<=V;j++)
        	    Bag[j]=max(Bag[j],Bag[j-v[i]]+c[i]);
		}
		else {
    
    
			for (int j=V;j>=v[i];j--)
		        for(int k=0;k<=n[i];k++)
		            if(j>=k*v[i])
		                Bag[j]=max(Bag[j],Bag[j-k*v[i]]+k*c[i]);
		}
    cout<<Bag[V];
    return 0;
}

2.4.3 简化

因为多重背包的输入可以简化,我也可以在此题的输入中做类似的操作来简化时间复杂度
如下:

#include <iostream>
#define maxn 1000
using namespace std;
int main() {
    
    
	int N,V;
	cin>>V>>N;
	int tmpv,tmpc,tmpk,v[N+1],c[N+1],n[N+1],cnt;
	for(int i=1;i<=N;i++) {
    
    
		cin>>tmpv>>tmpc>>tmpk;
		if(tmpk==0) {
    
    
			v[++cnt]=tmpv;
			c[cnt]=tmpc;
			n[cnt]=0;
		}
		else {
    
    
			for(int j=1;j<=tmpk;j<<=1) {
    
    
				tmpk-=j;
				v[++cnt]=j*tmpv;
				c[cnt]=j*tmpc;
				n[cnt]=-1;
			}
			if(tmpk>0) {
    
    
				v[++cnt]=tmpk*tmpv;
				c[cnt]=tmpk*tmpc;
				n[cnt]=-1;
			}
		}
	}
	int Bag[V+1]={
    
    0};
	for(int i=1;i<=cnt;i++) {
    
    
		if(n[i]==0) {
    
    
			for(int j=v[i];j<=V;j++) {
    
    
				Bag[j]=max(Bag[j],Bag[j-v[i]]+c[i]);
			}
		}
		else {
    
    
			for(int j=V;j>=v[i];j--) {
    
    
				Bag[j]=max(Bag[j],Bag[j-v[i]]+c[i]);
			}
		}
	}
	cout<<Bag[V];
	return 0;
}

2.5 小结

现在,01,完全,多重这三种基础的背包问题,以及他们的混合的问题已经介绍完了,希望大家能充分理解,谢谢
那么还有什么建议或者问题可以在评论区留言,另外,再次谢谢大家的阅读
另外,之后的问题只是略讲一番,
因为背包问题的思路已经全部介绍完了

2.6 二位费用背包

2.6.1 分析

那么继续介绍背包问题,二位费用,顾名思义就是指物品有两种代价,且两种代价不相关,那么,还是根据01的方程*2:

Bag[i][j]=max(Bag[i-1][j],Bag[i-1][j-v[i]]+c[i]);

推出

Bag[i][v][u]=max(Bag[i-1][v][u],Bag[i-1][v-a[i]][u-b[i]]+c[i]);

其中v,u是第一二维的最大可承受代价,a是该物品第一维的代价,b是该物品的第二维代价,c是该物品的收益

2.6.2 主要代码

如上所说,我们直接开三层循环
伪代码如下:

for i 1 to N i++
	for v V to a[i] v--
	△逆序循环
		for u U to b[i] u--
		△逆序循环
			Bag[v][u] <- max(Bag[v][u],Bag[v-a[i]][u-b[i]]+c[i]);
			△Bag[第一维的费用-当前物品的第一维费用][第二维的费用减去-当前物品的第二维费用]+物品的价值

2.6.3 完整代码

题目描述:
给定背包的容量,最大可承重和物品数量,以及每种物品的体积,重量和价值,求可得的最大价值
样例输入:
4 5 6
1 2 3
2 4 4
3 4 5
4 5 6
样例输出:
8
数据范围:
所有数据和输出都在1000范围之内
参考代码:

#include<iostream>
using namespace std;
int main()
{
    
    
	int n,V,U;
	cin>>n>>V>>U;
	int a[n+1],b[n+1],c[n+1],Bag[V+1][U+1]={
    
    0};
	for (int i=1;i<=n;i++) cin>>a[i]>>b[i]>>c[i];
	for (int i=1;i<=n;i++)
		for (int v=V;v>=a[i];v--)
			for (int u=U;u>=b[i];u--)
					Bag[v][u]=max(Bag[v][u],Bag[v-a[i]][u-b[i]]+c[i]);
	cout<<Bag[V][U];
}

2.7 分组的背包问题

2.7.1 分析

顾名思义,这个问题中的物品被分成数个组,但是明显不可能让你挑一个组去求,而是从各组中选一个出来,求所获得的最大价值
那很明显,第一层循环i应该遍历组数,第二层j遍历V,第三层k遍历i组中的物品
伪代码如下:

for i 1 to 分组总数 i++
	for j V to 0 j++
		for 遍历第i组的第k件物品
			Bag[j] <- max(Bag[j],Bag[j-v[i][k]]+c[i][k])

这里,k是遍历物品的循环,但是它和01不同的是,k在第三层,这样才能保证每个组只拿一个物品,赋值给Bag[j]

2.7.2 完整代码

题目描述:
给定背包的容量,物品组数,以及每组物品的个数,及组内物品的体积,价值,求可得的最大价值
样例输入:
3 10
3
3 2
2 3
2 2
2
5 4
6 4
3
1 2
2 1
4 3
样例输出:
9
数据范围:
所有数据和输出都在1000之内
参考代码:

#include <iostream>
#define maxn 1005
using namespace std;
int main() {
    
    
	int V,n;
    cin>>n>>V;
    int v[n+1][maxn],c[n+1][maxn],num[maxn];
	int Bag[V+1]={
    
    0};
    for(int i=1;i<=n;i++) {
    
    
        cin>>num[i];
        for(int j=1;j<=num[i];j++) {
    
    
            cin>>v[i][j]>>c[i][j];
        }
    }
    for(int i=1;i<=n;i++) {
    
    
        for(int j=V;j>=0;j--) {
    
    
            for (int k=1;k<=num[i];k++) {
    
    
                if (v[i][k]<=j) {
    
    
                    Bag[j]=max(Bag[j],Bag[j-v[i][k]]+c[i][k]);
                }
            }
        }
    }
    cout<<Bag[V];
    return 0;
}

2.8 小结

这里,背包的二次进阶玩法就完了
都只要骚微的改一下就完了
后面的问题,就涉及到树形dp,函数
我尽量把思路讲清,代码有点废肝*3

2.9 有依赖的背包问题

依赖的背包问题比较复杂,我尽量简单说说,首先,假设物品b依赖a,那么只有选了a,才能选b,当然所有物体也可能有依赖关系,这里讲一下一个物品只能有一个被依赖的物品或者依赖的物品*4
那么,我们从推01的方程的过程:
在这里插入图片描述
推出这里简单的方程:
第一个状态:

Bag[j]
//不拿主件

第二个状态:

Bag[j-v1[i]]+c1[i]
//只拿主件

第三种状态:

Bag[j-v1[i]-v2[i]]+c1[i]+c2[i]
//主副件同时拿

伪代码如下:

for i 遍历主件
	for j 遍历空间
		if 有附件 & j>=v1[i]+v2[i] △可以拿附件
			then Bag[j] <- max(Bag[j],Bag[j-v1[i]]+c1[i],Bag[j-v1[i]-v2[i]]+c1[i]+c2[i])
		else if j>=v1[i] △只能拿主件
			then Bag[j] <- max(Bag[j-v1[i]]+c1[i],Bag[j-v1[i]-v2[i]]+c1[i]+c2[i])

如果是将条件改成一个主件可以有多个附件,但附件不能有该物品的附件
则我们可以对当前物品的附件做一次01背包,设Ben[0~V-v[i]]为附件的dp结果,令k为遍历Ben的循环,那么v[i]+k为体积,Ben[k]+c[i]是价值,再一般一点就是树形dp
套娃是不可能的,代码就算了

2.10 泛化背包

这里的物品全是一个个函数,你给它多少空间(v),他就给你 (fun(v))的价值
还是从01的方程:

Bag[i][j]=max(Bag[i-1][j],Bag[i-1][j-v[i]]+c[i]);

推出:

Bag[j]=max(Bag[j],Bag[j-k]+c[i][k]);

其中k是用来遍历代价的循环,c[i][k]则是物品i所给代价k后的价值

2.11 01背包的方案总数

因为要求方案总数,那么我们可以额外定义Ste[i][j]来遍历Bag[i][j]问题的方案总数
伪代码如下:

Bag(0~n,0~V)的数据已就绪
Ste(0,0) <- 1 △重要一步
for i 1 to n i++
	for j 0 to V j++
		if Bag(i,j)==Bag(i-1,j)
			then Ste(i,j) <- Ste(i-1,j)
		if Bag(i,j)==Bag(i-1,j-v(i))+c(i)
			then Ste(i,j) <- Ste(i-1,j-v(i))

怎么样?是不是惊人的相似,因为计算方案总数的性质和计算答案差不多,区别在于一个求数据,一个求方案总数

3 总结

现在,你已经初步了解了背包问题,但更多的是练习,希望大家更了解背包,谢谢

猜你喜欢

转载自blog.csdn.net/weixin_49692699/article/details/108055962