背包问题

31 篇文章 0 订阅
22 篇文章 0 订阅

导语

背包问题是DP(动态规划)的入门题型,对我们从理解DP到熟练的掌握DP有着举足轻重的作用

题型分类

在这里插入图片描述
(以上是笔者本菜鸡知道的,应该是全的,若有不周,请包涵,

问题处理

①01背包

有 N 件物品和一个容量为 M 的背包。第i件物品的费用(即体积)是 w[i] ,价值是 v[i] 。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

分析:基础的DP,以DP的思维方式,我们是以空间换时间的,
由此,我们直接暴力的开一个三维数组也就是 f[i][w] ,i表示放到了第多少件物品,w是表示放到现在一共用了多少的空间,而 f[i][w] 的值就是放到目前可以得到的最大价值,对于每一组物品,我们是有两种选择的,“选”或者“不选”,由此,我们便可以的到状态转移方程 f[i][j] = max{ f[i - 1][j - w[i]] + v[i] , f[i][j] } (v[i]是这件物品的价值),
实现上面也就是暴力的三重循环把动态转移方程套进去了

但是,这样写的话,二维数组是否有一些浪费呢?
f[i][v] 是由 f[i-1][v] 和 f[i-1][v-w[i]] 两个子问题递推而来,能否保证在推 f[i][v] 时(也即在第i次主循环中推 f[v] 时)能够得到 f[i-1][v] 和 f[i-1][v-w[i]] 的值呢?
事实上,这要求在每次主循环中我们以 j = M … w[i] (到 w[i] 表示的是塞到最后用 w[i] 塞满了) 的逆序推 f[v] ,这样才能保证推 f[v] 时 f[v-w[i]] 保存的是状态 f[i-1][v-w[i]] 的值。
方程也便是: f[j] = max{ f[j - w[i]] + v[i] , f[j] }

代码实现

#include <bits/stdc++.h>
using namespace std;

long f[30000];
int m,n,v[30],w[28];

int main()
{
	memset(f,0,sizeof(f));      //数组初始化
	
	//输入
	cin>>m>>n;
	for (int i = 1; i <= n; i++)
		cin>>w[i]>>v[i];
		
	for (int i = 1; i <= n; i++)
		for (int k = m; k >= w[i]; k--)    //这是要保证第i次循环中的状态f[i][v]是由状态f[i-1][v-w[i]]递推而来
														/*
														换句话说,这正是为了保证每件物品只选一次,
														保证在考虑“选入第i件物品”这件策略时,
														依据的是一个绝无已经选入第i件物品的子结果	f[i-1][m-w[i]]	。
														*/
			f[k] = max(f[k],f[k - w[i]] + v[i]);       //套方程
			
	//输出
	cout<<f[m];
	
	return 0;
}

附带一个剪枝搜索的01背包吧

#include <bits/stdc++.h>
using namespace std;

bool vis[3009];
int v[3009],p[3009],judge[100000009];
int n,m,ans;

void search(int k,int cost,int get)
{
	if (judge[cost] > get) return;     //剪枝
		judge[cost] = get;
		
	if (k == n)    //所有的都试完了
		{
			ans = max(ans,get);
			
			return; 
		}
		
	for (int i = k+1; i <= n; i++)
		if (vis[i] && p[i] + cost <= m)
			{    //放
				vis[i] = false;
				
				search(k+1,cost+p[i],get+v[i]);     
				
				vis[i] = true;
			}
			else    //不放
				search(k+1,cost,get);
}

int main()
{
	memset(vis,true,sizeof(vis));
	
	cin>>m>>n;
	for (int i = 1; i <= n; i++)
		cin>>p[i]>>v[i];
		
	search(0,0,0);
	
	cout<<ans;
	
	return 0;
}

②完全背包

完全背包也就是01背包的加强版,具体的不同便是其中的每一件物品都可以在空间条件满足(包放的下)的条件下,无限放,求的也是最多的利益

分析:这里是可以无限放的,不正是与01背包只可以放一次或者不放的相反吗?
不也就是把向背包里塞的那一个循环倒过来吗?
但是,为什么可以倒过来呢?
而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果 f[i][m-w[i]] ,所以就可以并且必须采用 j = w[i] … M (从 w[i] 开始表示起码要占用 w[i] 的空间)的顺序循环。

因为是同一类问题,所以我们的动态转移方程也是 f[j] = max{ f[j - w[i]] + v[i] , f[j] }

代码实现

#include <bits/stdc++.h>
using namespace std;

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

int main()
{
	//输入
	cin>>m>>n;
	for (int i = 1; i <= n; i++)
		cin>>w[i]>>v[i];
		
	for (int i = 1; i <= n; i++)
		for (int j = w[i]; j <= m; j++)        //原理上面已经说过了
			f[j] = max(f[j],f[j-w[i]] + v[i]);      //套方程
			
	cout<<f[m];    //输出
	
	return 0;
} 

③多重背包
有 N 种物品和一个容量为 M 的背包。第i种物品最多有 num[i] 件可用,每件费用是 w[i] ,价值是 v[i] 。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
状态转移方程是一样的

分析:这个与前面几个问题的区别便是——每一个物品是有个数的(可以放多少次)
为此,我们直接多套一重循环枚举 num[i] 就可以了
动态转移方程便是:f[j] = max{ f[j] ,f[j - k * w[i]] + k * v[i] } ( k 表示这种物品放多少次 1 <= n <= num[i] )

代码实现

#include <bits/stdc++.h>
using namespace std;

int f[1000],w[1000],v[1000],num[1000];
int m,n;

int main()
{
	//输入
	cin>>n>>m;
	for (int i = 1; i <= n; i++)
		cin>>w[i]>>v[i]>>num[i];
		
	for (int i = 1; i <= n; i++)
		for (int j = m; j >= 0; j--)
			for (int k = 0; k <= num[i]; k++)     //枚举放多少个
				if (j >= k * w[i])               //判断能不能放的下 k 个第 i 种物品
					f[j] = max(f[j],f[j-k*w[i]] + k*v[i]);     //套方程
			
	cout<<f[m];    //输出
	
	return 0;
} 

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

问题分析:这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。
也就是说设 f[k][m] 表示前k组物品花费费用 m 能取得的最大权值,则有
f[k][m] = max{ f[k - 1][m],f[k - 1][M - w[i]] + v[i] (物品i 属于第k组)}
在实现上面,我们可以存一下每个物品的组别,在计算的部分也就是多套一重循环罢(但是要注意把对价值和对重量的循环对调,想不出原因 “ for j = M … 0 ”这一层循环必须在“for 所有的 i 属于组 k”之外。这样才能保证每一组内的物品最多只有一个会被添加到背包中。)
在实像上的状态转移方程,我们也可以滚动(状态压缩)一下,与01背包的压缩过后的方程一样的。

代码实现

#include<bits/stdc++.h>
using namespace std;

int m,n,t;
int w[1000], v[1000];
int groups[11][1000], f[1000];

int main()
{
	memset(f,0,sizeof(f));
	
	//输入
    cin>>m>>n>>t;
    for (int i = 1; i <= n; i++)
		{
			int num;
        	cin>>w[i]>>v[i]>>num;
        	
        	//对每一组的存储
			groups[num][0]++;
        	groups[num][grouds[num][0]] = i;
    	}
    	
    for (int k = 1; k <= t; k++)     //对所有组的尝试
        for (int j = m; j >= 0; j--)
            for (int i = 1; i <= groups[k][0]; i++)
                if (j >= w[groups[k][i]])       //判断能不能放
                        f[j] = max(f[j],f[j-w[groups[k][i]]] + v[groups[k][i]]);     //套方程
                    
    printf("%d",f[m]);    //输出
    
    return 0;
}

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

问题分析:可以形象的理解为:01背包多加了一个限制条件,也就是给 f 数组加一个维度,判断另一个条件,状态转移方程也就转化成了:f[j][z] = max{ f[j][z] , f[j - w1[i]][z - w2[i] ] + v[i] } ( j 同 M1,z 同 M2 )

代码实现

#include <bits/stdc++.h>
using namespace std;

int f[409][409],p[599],q[599],v[599];
int m1,n,m2;

int main()
{
	memset(f,0,sizeof(f));
	
	//输入
	cin>>m1>>m2;        //两个限制条件
	cin>>n;
	for (int i = 1; i <= n ; i++)
		cin>>w1[i]>>w2[i]>>v[i];

	for (int i = 1; i <= n; i++)
		for (int j = m1; j >= w1[i]; j--)      //两个限制条件的填充
			for (int z = m2; z >= w2[i]; z--)
				f[j][z] = max(f[j][z],f[j-w1[i]][z-w2[i]] + v[i]);     //状态转移方程

				
	cout<<f[m1][m2];     //输出

	return 0;
}

⑥统计填充方案数
对于一个给定了背包容量、物品费用、物品间相互关系(分组、依赖等)的背包问题,除了再给定每个物品的价值后求可得到的最大价值外,还可以得到装满背包或将背包装至某一指定容量的方案总数。

问题分析:这种求填充方案的背包一般只需将状态转移方程中的max改成sum即可。例如若每件物品均是01背包中的物品,转移方程即为 f[j] = sum{ f[j], f[M - w[i]] + v[i] } ,初始条件 f[0] = 1。
这个问题用语言难以表达,所以我们代入题目,使之能更简易的去理解。

例题
给你一个n种面值的货币系统,求组成面值为m的货币有多少种方案。
【样例输入】
3 10 //3种面值组成面值为10的方案
1 //面值1
2 //面值2
5 //面值5
【样例输出】
10 //有10种方案

解析:我们可以设 f[j] 为当前总面值到 j 最大的方案总数,依据01背包的转移方程,我们不难可以得出
f[j] = Σf[j - a[i]] (a[i] 是第 i 个钞票的面值)

代码实现

#include <bits/stdc++.h>
using namespace std;

int n,m;
int note[1001];
long long f[10001];            

int main()
{
	//输入
    cin>>n>>m;
    for (int i = 1; i <= n; i++)
       cin>>note[i];
       
    f[0] = 1;    //初始化,组成0面值的钞票的方法的只有一种
    for (int i = 1; i <= n; i++)      //枚举 n 种面值
       for (int j = note[i]; j <= m; j++)      //看可以怎么放
          f[j] += f[j-note[i]];        //套动态转移方程
    
	cout<<f[m];       //输出
    
	return 0;
}

⑦混合背包
混合背包也就是把 多重背包 ,完全背包 融合在了一起,
在这里,我们对于一件物品,若它可以使用的次数为 k[i] = 0,就表示它有无限个,可以再空间条件允许的情况下,无限扔进背包里,反之,若其个数 k[i] > 0,就表示这个物品可以用 k[i] 次,
同样,我们要求最大利润

问题分析:对于这种问题,我们可以开一个 if 来判断一下,判断这个物品是该用完全背包的方式来计算,还是用多重背包的方式来计算,状态转移方程也还是老样子:f[j] = max{ f[j - w[i]] + v[i] , f[j] } 但是有还有一个 f[j] = max{ f[j] ,f[j - k * w[i]] + k * v[i] } 这个是多重背包的

代码实现

#include <bits/stdc++.h>
using namespace std;

int f[1000],w[1000],v[1000],num[1000];
int m,n;

int main()
{
	//输入
	cin>>m>>n;
	for (int i = 1; i <= n; i++)
		cin>>w[i]>>v[i]>>num[i];
		
	for (int i = 1; i <= n; i++)
		if (num[i] == 0)                 //完全背包的情况
			for (int j = w[i]; j <= m; j++)
				f[j] = max(f[j],f[j-w[i]] + v[i]);    //套方程
			else                              //多重背包的情况
			for (int j = m; j >= 0; j--)
				for (int k = 0; k <= num[i]; k++)
					if (j >= k * w[i])
						f[j] = max(f[j],f[j-k*w[i]] + k*v[i]);    //套方程
			
	cout<<f[m];    //输出
	
	return 0;
} 

⑧有依赖性背包
这是一类比较难得背包问题,笔者也是懂一些皮毛,, 所以在这里就引用一个比较经典的PPT里的东西了:

简化的问题
这种背包问题的物品间存在某种“依赖”的关系。也就是说,i依赖于j,表示若选物品i,则必须选物品j。为了简化起见,我们先设没有某个物品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多件物品。
算法
这个问题由NOIP2006金明的预算方案一题扩展而来。遵从该题的提法,将不依赖于别的物品的物品称为“主件”,依赖于某主件的物品称为“附件”。由这个问题的简化条件可知所有的物品由若干主件和依赖于每个主件的一个附件集合组成。
按照背包问题的一般思路,仅考虑一个主件和它的附件集合。可是,可用的策略非常多,包括:一个也不选,仅选择主件,选择主件后再选择一个附件,选择主件后再选择两个附件……无法用状态转移方程来表示如此多的策略。(事实上,设有n个附件,则策略有2^n+1个,为指数级。)
考虑到所有这些策略都是互斥的(也就是说,你只能选择一种策略),所以一个主件和它的附件集合实际上对应于分组的背包中的一个物品组,每个选择了主件又选择了若干个附件的策略对应于这个物品组中的一个物品,其费用和价值都是这个策略中的物品的值的和。但仅仅是这一步转化并不能给出一个好的算法,因为物品组中的物品还是像原问题的策略一样多。
再考虑分组的背包中的一句话: 可以对每组中的物品应用完全背包中“一个简单有效的优化”。这提示我们,对于一个物品组中的物品,所有费用相同的物品只留一个价值最大的,不影响结果。所以,我们可以对主件i的“附件集合”先进行 一次01背包,得到费用依次为0…V-w[i]所有这些值时相应的最大价值f’[0…V-w[i]]。那么这个主件及它的附件集合相当于V-w[i]+1个物品的物品组,其中费用为w[i]+k的物品的价值为f’[k]+c[i]。也就是说原来指数级的策略中有很多策略都是冗余的,通过一次01背包后,将主件i转化为 V-w[i]+1个物品的物品组,就可以直接应用分组的背包的算法解决问题了。
更一般的问题是:依赖关系以图论中“森林”的形式给出(森林即多叉树的集合),也就是说,主件的附件仍然可以具有自己的附件集合,限制只是每个物品最多只依赖于一个物品(只有一个主件)且不出现循环依赖。
解决这个问题仍然可以用将每个主件及其附件集合转化为物品组的方式。唯一不同的是,由于附件可能还有附件,就不能将每个附件都看作一个一般的01 背包中的物品了。若这个附件也有附件集合,则它必定要被先转化为物品组,然后用分组的背包问题解出主件及其附件集合所对应的附件组中各个费用的附件所对应的价值。
事实上,这是一种树形DP,其特点是每个父节点都需要对它的各个儿子的属性进行一次DP以求得自己的相关属性。这已经触及到了“泛化物品”的思想。看完后,你会发现这个“依赖关系树”每一个子树都等价于一件泛化物品,求某节点为根的子树对应的泛化物品相当于求其所有儿子的对应的泛化物品之和。
小结
NOIP2006的那道背包问题,通过引入“物品组”和“依赖”的概念可以加深对这题的理解,还可以解决它的推广问题。用物品组的思想考虑那题中极其特殊的依赖关系:物品不能既作主件又作附件,每个主件最多有两个附件,可以发现一个主件和它的两个附件等价于一个由四个物品组成的物品组,这便揭示了问题的某种

总结

这个blog写了一晚上累死了,mmp
背包问题是经典的,带着一层薄纱,使我们挥洒好奇的心去揭开,细细品味其中DP算法之美 ~~(是不是很有文采, d=====( ̄▽ ̄*)b)~~
学习DP的路还有很长,
即使路漫漫其修远兮,但吾仍将上下而求索!!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值