背包问题总结
暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!
背包问题是动态规划(Dynamic Planning) 里的非常重要的一部分,关于几种常见的背包,其关系如下:
在解决背包问题的时候,我们通常都是按照如下五部来逐步分析,把这五步都搞透了,算是对动规来理解深入了。
1)动规四部曲:
(1) 确定dp数组及其下标的含义
(2) 确定递推公式
(3) dp数组的初始化
(4) 确定遍历顺序
01背包问题
给定n种物品,和一个容量为C的背包,物品i的重量是w[i],其价值为v[i]。问如何选择装入背包的物品,使得装入背包中的总价值最大?(面对每个物品,只能有选择拿取或者不拿两种选择,不能选择装入某物品的一部分,也不能装入物品多次)
声明一个数组f[n][c]的二维数组,f[i][j]表示在面对第i件物品,且背包容量为j时所能获得的最大价值。
根据题目要求进行打表查找相关的边界和规律
根据打表列写相关的状态转移方程
用程序实现状态转移方程
真题演练:
一个旅行者有一个最多能装M公斤的背包,现在有n件物品,它们的重量分别是W1、W2、W3、W4、…、Wn。它们的价值分别是C1、C3、C2、…、Cn,求旅行者能获得最大价值。
输入描述:
第一行:两个整数,M(背包容量,M<= 200)和N(物品数量,N<=30);
第2…N+1行:每行两个整数Wi,Ci,表示每个物品的质量与价值。
输出描述:
仅一行,一个数,表示最大总价值
样例:
输入:
10 4
2 1
3 3
4 5
7 9
输出:
12
解题步骤:
动规四部曲:
1) 确定dp数组及其下标的含义
dp[i][j] 表示从下标为 [0 - i] 的物品里任意取,放进容量为j的背包,价值总和最大是多少。
2) 确定递推公式
不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以被背包内的价值依然和前面相同。)
放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值
所以递归公式:
如果当前背包的容量可以装下物品:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
如果当前背包的容量装不下该物品:
dp[i][j]=dp[i-1][j];
3)dp数组的初始化
* 首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0
* 状态转移方程 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。
* dp[0][j]:存放编号0的物品的时候,各个容量的背包所能存放的最大重量j。
那么很明显当 j < weight[0]时,dp[0][j] 应该是 0(背包容量比编号0的物品重量还小)
同理,当j >= weight[0]时,dp[0][j] 应该是value[0](背包容量足够放编号0物品)
源码:
#include<stdio.h>
int max(int x,int y)
{
if(x>=y) return x;
else return y;
}
int main()
{
int N,V;
scanf("%d %d",&N,&V);
int v[1001]={0},w[1001]={0};
for(int i=1;i<=N;i++)
{
scanf("%d %d",&v[i],&w[i]);
}
int dp[1001][1001]={0};
for(int i=1;i<=N;i++)
{
for(int j=1;j<=V;j++)
{
if(v[i]>j){
dp[i][j]=dp[i-1][j];
}
else{
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
}
}
}
printf("%d",dp[N][V]);
return 0;
}
完全背包
有 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
解题思路:
朴素写法(三重循环):
动态规划数组定义:
使用二维数组f[i][j],其中f[i][j]表示考虑前i件物品,当背包容量为j时的最大价值。(和01背包一样)
状态转移方程:
f [ i ] [ j ] = max( f [ i ] [ j ], f [ i - 1][ j - v [ i ] * k ] + w [ i ] * k),这个方程考虑了不选取当前物品和选取当前物品k次两种情况下的最大价值,并取这些情况的最大值更新DP数组。
循环遍历:
外层循环遍历所有物品,中层循环遍历所有可能的背包容量。
内层循环遍历每个物品的可能选取次数k(即物品i可以被选取0次、1次、2次,直到k * v[i]超过当前背包容量j)。
对于每种情况,更新f[i][j]为max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k),表示考虑选取k次物品i时的最大价值。
源码:
#include<stdio.h>
int max(int a,int b){
if(a>=b) return a;
else return b;
}
int main()
{
int N,V0;
scanf("%d %d",&N,&V0);
int v[1001],w[1001];
for(int i=1;i<=N;i++){
scanf("%d %d",&v[i],&w[i]);
}
int dp[1001][1001]={0};
for(int i=1;i<=N;i++){
for(int j=1;j<=V0;j++){
if(j>=v[i]){
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
for(int k=1;k<=j/v[i];k++){
dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]*k]+w[i]*k);
}
}
else{
dp[i][j]=dp[i-1][j];
}
}
}
printf("%d",dp[N][V0]);
return 0;
}