算法学习-背包问题

一、01背包问题
有N件物品和一个容量为V的背包。放入第i件物品花费的费用是c[i],得到的价值是w[i],求将哪些物品装入背包可使价值总和最大。
直接给出01背包问题的二维动态方程:

f[i][j]=max(f[i][j],f[i-1][j-c[i]]+w[i]);

怎么理解呢?
想象出一个二维数组f[0…N][0…V],其中f[i][j]表示前i个物品装入背包容量为j的方案中价值最大的。那么整个问题就可以规划成很多子问题,只需要抓住关键:
1、若第i个物品无法装入(即第i个物品的容量超过了背包容量),那么f[i][j]=f[i-1][j],相当于前i-1个物品装入背包容量为j的方案中价值最大的;
2、若第i个物品可以装入,该问题就可以转化为将第i个物品装入后,剩余了j-c[i]的容量用来装前i-1个物品的最大价值,即f[i][j]=f[i-1][j-c[i]]+w[i]。

从上面叙述中我们可以知道,f[i][j]的推出需要借助前i-1个物品装入容量为j的背包或者容量为j-c[i]的背包的最大价值。因此求解整个f[0…N][0…V]的循环代码应该如下:

for(int i=1;i<=N;i++)
	for(int j=0;j<=V;j++)
		f[i][j]=max(f[i-1][j],f[i-1][j-c[i]]+w[i]);

时间复杂度是O(N*V)。
如何优化?直接对这个二维数组优化时间无法实现,但是我们可以优化空间。优化代码如下:

for(int i=1;i<=N;i++)
	for(int j=V;j>=0;j--)
		f[j]=max(f[j],f[j-c[i]]+w[i]);

这样,二维数组就变成了一维数组,实现了空间的优化。
原理是怎样的呢?
实际上,在二维的求解每个f[i][j]时,利用的是前i-1个物品装入背包的最大价值,那么可以直接使用一维数组实现覆盖,要么就是原封不动的f[j],要么就是f[j-c[i]]+w[i]。

观察上述的第二个for循环,j是从V…0这样逆序枚举的,这样的意义何在?
实际上,在求解f[j]时,也就是相当于二维中的f[i][j],那么我们就需要知道f[i-1][j]和f[i-1][j-c[i]]两个状态,利用一维数组的关键是“覆盖”,每次求主循环为i时的f[j]都得覆盖主循环为i-1时的f[j]。
但是只有使用逆序,f[j-c[i]]才算是前i-1个物品装入背包的最大价值,这样你才能放心大胆的用f[j]去覆盖它。否则,在主循环到i时,f[j-c[i]]已经是f[i][j-c[i]],并不符合题意。
再做个简单的优化,代码如下(注意比较):

for(int i=1;i<=N;i++)
	for(int j=V;j>=c[i];j--)
		f[j]=max(f[j],f[j-c[i]]+w[i]);

从上面理解完覆盖的思想以后,可以很清楚的理解这个优化:既然物品i的代价大于背包容量j后,实际上f[j]=max(f[j],f[j-c[i]]+w[i])的结果一定是f[j],即前i-1个物品装入容量为j的背包的最大价值,无须覆盖。

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

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8

代码如下:

#include<bits/stdc++.h>
using namespace std;
int f[1005];
int main(){
    int n,v;
    scanf("%d%d",&n,&v);
    int *c=new int[n+1];
    int *w=new int[n+1];
    for(int i=1;i<=n;i++)scanf("%d%d",&c[i],&w[i]);
    for(int i=1;i<=n;i++)
        for(int j=v;j>=c[i];j--)
            f[j]=max(f[j],f[j-c[i]]+w[i]);
    printf("%d\n",f[v]);
}

关键是题目要求每个物品只有一个,并不能重复装。记住这一点,接下来将和完全背包问题作比较。

二、完全背包问题
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i]c[i],价值是w[i]w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
完全背包问题与01背包问题不同的地方是每件物品都有无限件可用。那么对于每件物品,都可以选k个,其中k满足k*c[i]<=j,那么二维的状态转移方程为:

f[i][j]=max(f[i-1][j],f[i-1][j-kc[i]]+kw[i]);(k*c[i]<=j)

最简单的代码可以这么写:

for(int i=1;i<=N;i++)
	for(int j=0;j<=V;j++)
		for(int k=1;k*c[i]<=j;k++)
			f[i][j]=max(f[i-1][j],f[i-1][j-k*c[i]]+k*w[i]);

其中f[i][j]表示前i种物品装入容量为j的背包的最大价值,注意是前i种物品不是前i个,可重复选同一物品多次。
优化思想:把完全背包问题转化为对01背包问题的求解,对每种物品i,最多选择V/c[i]个,于是可以转化为V/c[i]个相同代价和价值的物品,代码如下:

for(int i=1;i<=N;i++)
	for(int j=c[i];j<=V;j++)
		f[j]=max(f[j],f[j-c[i]]+w[i]);

注意观察这个和01背包的不同之处:这里的第二层循环是顺序的,这是因为每个物品都可以重复选,因此对每种物品i(主循环),可以多次询问,覆盖,状态方程实际上可以理解为:

f[i][j]=max(f[i-1][j],f[i][j-c[i]]+w[i])

如果是二维数组的话,其实第二层循环顺序逆序都没有关系,只要满足状态方程即可,但是为了优化空间,利用一维数组实现多次覆盖操作,那就需要我们理解掌握第二层循环顺逆序的问题了。

由这个优化思想可以更多地拓展:
把第i个物品拆分成若干个代价为 c[i]×2^k ,价值为w[i]× 2^k 的若干物品,其中k满足c[i]×2^k<=V
这利用了二进制的思想,因为实际上无论要选多少个i物品,都可看做若干个二进制数的和(如果要选5个物品i,就可以选择一个c[i]×2^0和 c[i]×2^2)。这样就使得各个物品各不相同,问题就转化成了01背包问题。
例题
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。第 i 种物品的体积是vi,价值是wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。

输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10

代码如下:

#include<bits/stdc++.h>
using namespace std;
int f[1005];
int main(){
    int n,v;
    scanf("%d%d",&n,&v);
    int *c=new int[n+1];
    int *w=new int[n+1];
    for(int i=1;i<=n;i++)scanf("%d%d",&c[i],&w[i]);
    for(int i=1;i<=n;i++)
        for(int j=c[i];j<=v;j++)
            f[j]=max(f[j],f[j-c[i]]+w[i]);
    printf("%d\n",f[v]);
}

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

这个问题和完全背包相比有类似之处,完全背包问题每种物品可无限次选择,只需满足k*c[i]<=V即可,相比之下,多重背包问题在完全背包的基础上增加了一个限制条件即k<=p[i]。最简单的一维代码很容易写出:

for(int i=1;i<=N;i++)
	for(int j=V;j>=c[i];j--)
		for(int k=1;k<=p[i]&&k*c[i]<=j;k++)
			f[j]=max(f[j],f[j-k*c[i]]+k*w[i]);

优化:利用前面对完全背包问题的拓展优化,即利用二进制思想,但是必须满足限制:取的物品i数量不能超过p[i]。代码如下:

for(int i=1;i<=N;i++){
	int num=min(p[i],V/c[i]);
	for(int k=1;num>0;k<<=1){
		if(k>num)k=num;
		num-=k;
		for(int j=V;j>=k*c[i];j--)
			f[j]=max(f[j],f[j-k*c[i]]+k*w[i]);
	}
}

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

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V≤100
0<vi,wi,si≤100
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10

代码如下:

#include<bits/stdc++.h>
using namespace std;
int f[1005];
int main(){
    int N,V;
    scanf("%d%d",&N,&V);
    int *c=new int[N+1];
    int *w=new int[N+1];
    int *s=new int[N+1];
    for(int i=1;i<=N;i++)scanf("%d%d%d",&c[i],&w[i],&s[i]);
    for(int i=1;i<=N;i++){
        int num=min(s[i],V/c[i]);
        for(int k=1;num>0;k<<=1){
            if(k>num)k=num;
            num-=k;
            for(int j=V;j>=k*c[i];j--)
                f[j]=max(f[j],f[j-k*c[i]]+k*w[i]);
        }
    }
    printf("%d\n",f[V]);
}

用二进制思想来优化算法已经是本菜鸡掌握的最好的方法了,还有一种更牛的是利用单调队列,过一段时间掌握了再分享。

四、混合背包问题
将前面三个背包问题混合起来
只需要区分清楚顺逆序即可,直接给出代码(p[i]为0是表示该物品只能取一次,为-1时表示该物品可以取无数次,否则就是最多可以取p[i]件物品i):

for(int i=1;i<=N;i++){
	if(p[i]==0)//01背包
		for(int j=V;j>=c[i];j--)
			f[j]=max(f[j],f[j-c[i]]+w[i]);
	else if(p[i]==-1)//完全背包
		for(int j=c[i];j<=V;j++)
			f[j]=max(f[j],f[j-c[i]]+w[i]);
	else{//多重背包
		int num=min(p[i],V/c[i]);
		for(int k=1;num>0;k<<=1){
			if(k>num)k=num;
			num-=k;
			for(int j=V;j>=k*c[i];j--)
				f[j]=max(f[j],f[j-k*c[i]]+k*w[i]);
		}
	}
}

例题
有 N 种物品和一个容量是 V 的背包。
物品一共有三类:
第一类物品只能用1次(01背包);
第二类物品可以用无限次(完全背包);
第三类物品最多只能用 si 次(多重背包);
每种体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。
si=−1 表示第 i 种物品只能用1次;
si=0 表示第 i 种物品可以用无限次;
si>0 表示第 i 种物品可以使用 si 次;
输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V≤1000
0<vi,wi≤1000
−1≤si≤1000
输入样例
4 5
1 2 -1
2 4 1
3 4 0
4 5 2
输出样例:
8

代码如下:

#include<bits/stdc++.h>
using namespace std;
int f[1005];
int main(){
    int n,v,c,w,s;
    scanf("%d%d",&n,&v);
    for(int i=1;i<=n;i++){
        scanf("%d%d%d",&c,&w,&s);
        if(s==-1)
            for(int j=v;j>=c;j--)
                f[j]=max(f[j],f[j-c]+w);
        else if(s==0)
            for(int j=c;j<=v;j++)
                f[j]=max(f[j],f[j-c]+w);
        else{
            int num=min(s,v/c);
            for(int k=1;num>0;k<<=1){
                if(k>num)k=num;
                num-=k;
                for(int j=v;j>=k*c;j--)
                    f[j]=max(f[j],f[j-k*c]+k*w);
            }
        }
    }
    printf("%d\n",f[v]);
}

五、二维费用背包问题
对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问如何选择物品可以得到最大的价值。设第i件物品所需的两种代价分别为c[i]和g[i]。两种代价可付出的最大值(两种背包容量)分别为V和M。物品的价值为w[i]。

这里的状态方程就需要再添加一维了,三维状态方程为:

f[i][j][k]=max(f[i-1][j][k],f[i-1][j-c[i]][k-g[i]]+w[i])

其中,f[i][j][k]代表前i个物品付出两种最大代价分别为j,k时的最大价值。
依据前述关于覆盖的思想,求解代码可写为:

for(int i=1;i<=N;i++)
	for(int j=V;j>=c[i];j--)
		for(int k=M;k>=g[i];k--)
			f[j][k]=max(f[j][k],f[j-c[i]][k-g[i]]+w[i]);

该类型的题目可能会隐含地给出,例如在一般的01背包问题上加上限定条件:所选物品的数量不大于M,实际上可以理解为每个物品除了代价c[i]以外,还有g[i],且g[i]皆为1,装入另一个容量为M的背包,那么直接添加第三层循环为:

for(int K=M;k>=1;k–)

那么f[j][k]表示付出费用j,最多选不超过k件物品时的最大价值。

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

输入格式
第一行三个整数,N,V,M,用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。接下来有 N 行,每行三个整数 vi,mi,wi,用空格隔开,分别表示第 i 件物品的体积、重量和价值。

输出格式
输出一个整数,表示最大价值。

数据范围
0<N≤1000
0<V,M≤100
0<vi,mi≤100
0<wi≤1000
输入样例
4 5 6
1 2 3
2 4 4
3 4 5
4 5 6
输出样例:
8

代码如下:

#include<bits/stdc++.h>
using namespace std;
int f[105][105];
int main(){
    int N,V,M,v,m,w;
    scanf("%d%d%d",&N,&V,&M);
    for(int i=1;i<=N;i++){
        scanf("%d%d%d",&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);
    }
    printf("%d\n",f[V][M]);
}

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

直接和01背包做比较吧,01背包处理的关键是求解每个f[i][j]时,有两种情况,第i个物品放入和不放入。而分组背包中的f[i][j]的两种情况则是第i组中选一件或者是不选,那么状态方程为:

f[i][j]=max(f[i-1][j],f[i-1][j-c[i]]+w[i])

形式和01背包相同,但是理解起来还是有区别的。继续使用滚动数组(实现覆盖),求解代码如下(由于因素包含每组物品数、代价、价值,略显复杂,直接用一个结构体变量Node表示吧):

结构体变量构造代码:

typedef struct{
	int c[1005];//代价
	int v[1005];//价值
	int s;//每组的物品数
}Node;
Node node[1005];

求解方程代码:

for(int i=1;i<=N;i++){
	scanf("%d",&node[i].s);//第i组的物品数量
	for(int j=1;j<=node[i].s;j++)scanf("%d%d",&node[i].c[j],&node[i].w[j]);
}
for(int i=1;i<=N;i++)//有N个组
	for(int j=V;j>=0;j--)
		for(int k=1;k<=node[i].s;k++)
			if(j>=node[i].c[k])
				f[j]=max(f[j],f[j-node[i].c[k]]+node[i].w[k]);
				//因为每组只取一个物品,因此可以覆盖前一次组内物品实现的最优解

实际上,多重背包是分组背包的特殊情况,多重背包只是每一组的物品都相同罢了。

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

输入格式
第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。
接下来有 N 组数据:
每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量;
每组数据接下来有 Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i 个物品组的第 j 个物品的体积和价值;

输出格式
输出一个整数,表示最大价值。

数据范围
0<N,V≤100
0<Si≤100
0<vij,wij≤100
输入样例
3 5
2
1 2
2 4
1
3 4
1
4 5
输出样例:
8

代码如下:

#include<bits/stdc++.h>
using namespace std;
int f[105];
int main(){
    int N,V,s;
    scanf("%d%d",&N,&V);
    for(int i=1;i<=N;i++){
        scanf("%d",&s);
        int *v=new int[s+1];
        int *w=new int[s+1];
        for(int j=1;j<=s;j++)scanf("%d%d",&v[j],&w[j]);
        for(int j=V;j>=0;j--)//对象是第i组
            for(int k=1;k<=s;k++)//注意这两层循环的次序
                if(j>=v[k])
                    f[j]=max(f[j],f[j-v[k]]+w[k]);
        //先执行背包容量的逆序循环再执行第i组的物品数循环
        //这样才能实现每组最多选1个物品
    }
    printf("%d\n",f[V]);
}

要好好理解琢磨一下滚动数组的思想!

七、有依赖的背包问题
物品间存在依赖关系,i和j存在依赖关系,则若选i,必选j。
实力不够,暂且搁置。

八、背包问题求方案数
实际上还是状态转移的思想,以01背包为例,用cnt数组来表示方案数,f数组表示最大价值,则

f[j]表示背包容量为j时的最大价值,cnt[j]表示背包容量为j时使得价值最大的方案数

在主循环中,当第i个物品加入后,若总价值变大了,那么肯定是把这个物品添加进去了,此时cnt[j]=cnt[j-v],也就是相当于背包容量为j-v的最大价值的方案数,在此基础上加上物品i,并不影响方案数。而如果总价值不变,那么cnt[j]+=cnt[j-v],也就是相当于求实现j容量的总价值最大方案时,直接加上j-v容量的总价值最大方案。代码如下:

for(int i=1;i<=N;i++)
	for(int j=V;j>=c[i];j--){
		int value=f[j-c[i]]+w[i];
		if(value>f[j])
			f[j]=value,cnt[j]=cnt[j-c[i]];
		else if(value==f[j])
			cnt[j]+=cnt[j-c[i]];
	}

其中要注意的是cnt初始值为1,因为价值为0即什么都不装时也是一种方案。
例题
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出 最优选法的方案数。注意答案可能很大,请输出答案模 109+7 的结果。

输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式
输出一个整数,表示 方案数 模 109+7 的结果。

数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 6
输出样例:
2

代码如下:

#include<bits/stdc++.h>
using namespace std;
#define MOD 1000000007
const int mod=1e9+7;
int f[1050],cnt[1050];
int main(){
    int N,V,v,w;
    scanf("%d%d",&N,&V);
    for(int i=0;i<=V;i++)cnt[i]=1;
    for(int i=1;i<=N;i++){
        scanf("%d%d",&v,&w);
        for(int j=V;j>=v;j--){
            int value=f[j-v]+w;
            if(value>f[j])
                f[j]=value,cnt[j]=cnt[j-v];
            else if(value==f[j])
                cnt[j]=(cnt[j]+cnt[j-v])%mod;
        }
    }
    printf("%d\n",cnt[V]);
}

最后再给一道最近在HNUCM刷到的一道板子题吧:
挖掘宝石
题目描述
Kimi设计了一个挖掘宝石的小游戏。在游戏中有红宝石、蓝宝石、绿宝石等多种不同类型的宝石,当然也有昂贵的钻石。
现在给出一个地图,在地图上有N种不同的宝石。每一种宝石都有一颗或者多颗,同一种宝石每一颗的价值都是相同的。
此外,每一种宝石都有一个挖掘时间。
在给定的时间内,哪一个玩家挖掘的宝石的总价值最大就是游戏的赢家。
现在给出N类不同宝石的数量以及每一类宝石中每一颗的价值和挖掘时间,并且给出一个总的游戏时间T。在不考虑竞争对手的情况下,请问可以得到的最大价值是多少?

输入
单组输入。
第1行输入一个正整数N和一个正整数T,分别表示宝石类型的数量和总游戏时间(分钟),两者之间用空格隔开。N<=100,T<=1000。
从第2行到第N+1行每一行三个正整数X[i],Y[i]和Z[i],分别表示第i类宝石的数量、第i类宝石中一颗宝石的价值和挖掘时间(分钟)。X[i]、Y[i]和Z[i]均不超过100。

输出
输出可以得到的最大价值。

样例输入
3 10
2 5 5
3 4 3
2 8 6

样例输出
12

代码如下:

#include<bits/stdc++.h>
using namespace std;
#define MOD 1000000007
int f[1050];
int main(){
    int N,T,x,y,z;
    scanf("%d%d",&N,&T);
    for(int i=1;i<=N;i++){
        scanf("%d%d%d",&x,&y,&z);
        int num=min(x,T/z);
        for(int k=1;num>0;k<<=1){
            if(k>num)k=num;
            num-=k;
            for(int j=T;j>=k*z;j--)
                f[j]=max(f[j],f[j-k*z]+k*y);
        }
    }
    printf("%d\n",f[T]);
}

  • 21
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

布布要成为最负责的男人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值