动态规划——背包问题

题记:在日本人的书上为动态规划问题下了一个简短而明确的定义——记录结果再用

内容大多来自背包九讲(如有侵权,立即删除)

所涉及背包内容:

1.01背包

2.完全背包

3.多重背包(朴素版本的首先讲解,主要是多重背包的二进制优化版本,优先队列优化版本等作者学会了再进行补充)

4.混合背包

5.二维费用的背包问题

6.分组背包

7.背包问题的方案数

8.输出背包的具体方案

9.有依赖的背包问题

一、01背包问题

作为背包问题的基础,必须要花费一些精力去弄懂01背包问题,否则剩下的8种背包都是基于01背包问题,将会很难理解。

首先我们来了解什么是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
输出样例:
8

题目链接:01背包问题

动态规划的精髓在于缩小问题规模,并记录下小规模问题的答案,用此来求解大规模问题——即最优子问题结构。

在01背包中,所需要记录的状态并不难想,因为大规模的问题就是n(物品的个数),m(背包的体积)非常大的时候,所以以此我们便可以确定状态为物品个数和背包体积。

那么,我们该怎么记录小规模问题的结果呢?

没错,用数组,dp[i][j]数组的值代表在只能取前(i-1)个物品且背包体积为j的情况下,所能得到的最大价值。

如何记录结果已经明白了,那么该如何解决结果的正确性呢?

到这我们就不得不提一个东西——状态转移方程。

像01背包这样的问题,无非两种选择,拿或者不拿当前第i个物品,那么状态转移方程就非常好写了:

dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i])(当前背包容量足够放第i个物品)

(当然了,如果不够放那么很显然就应该是dp[i][j]=dp[i-1][j],因为你没有取的可能性)

dp[i-1][j]代表不取当前第i个物品,直接用先前已经计算好的前i-1个物品且背包容量同为j时的最优解来代表当前的最优解

dp[i-1][j-v[i]]+w[i]代表取当前第i个物品,并且用先前已经计算好的前i-1个物品且背包容量为j-v[i]时的最优解来代表当前的最优解。

将两种状态取最大值,便可以代表当前取前i个物品且背包容量为j时的最优解了

对于样例,有这样一张表格:

在这里插入图片描述

而这张表格如何得到的呢,我先做一个示范,如dp[4][5]这个位置,也就是我们最终答案的位置,他应该是由 max(dp[3][5],dp[3][5-4]+5)所转移过来的,很显然前者为8,而后者为7,所以最终答案为8,具体每个格子该如何转移,我认为应该自己动手去理解一遍这个过程。

放上一个大佬的博客,里面详细的写了每一步如何转移:0—1背包

而代码也十分简洁明了:

#include<iostream>
#include<iomanip>
using namespace std;
const int MAXN=1010;
int dp[MAXN][MAXN],n,m,v[MAXN],w[MAXN];
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>v[i]>>w[i];
	}
	for(int i=1;i<=n;i++){//当前有i个物品可供选择
		for(int j=1;j<=m;j++){//当前的背包容量为j
			if(j>=v[i]) dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);//如果足够放,使用状态转移方程求解
			else dp[i][j]=dp[i-1][j];//不够放就直接将上一行的值拿下来
		}
	}
	cout<<dp[n][m]<<endl;//输出有n个物品且背包容量为m时的最优结果,即为答案
	return 0;
}

这就是最基础的01背包做法了,时间复杂度为O(n*m)。

想必看到这,聪明的你们就发现了:好像每次状态转移只用到了当前行和上一行的值诶,那我可不可以只开一个只有两行的数组来求解呢?

答案是可以的,并且这样做可以大大节省空间。

代码如下:

#include<iostream>
#include<iomanip>
using namespace std;
const int MAXN=1010;
int dp[2][MAXN],n,m,v[MAXN],w[MAXN];
int main(){
	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++){
			if(j>=v[i]) dp[1][j]=max(dp[0][j],dp[0][j-v[i]]+w[i]);
			else dp[1][j]=dp[0][j];
		}
		for(int j=1;j<=m;j++) dp[0][j]=dp[1][j];//当前行在下次运算中就变成了上一行,所以将当前行赋值给上一行
	}
	cout<<dp[1][m]<<endl;
	return 0;
}

好了现在问题又来了!既然可以把他放在两行,那么我可不可以再减去一行!只用一个一位数组,就顺利求出问题的解呢?

答案又是可以的,很神奇吧?

只不过我们需要更改内重循环j的枚举顺序,把 for(int j=1;j<=m;j++) 变成 for(int j=m;j>=v[i];j- - )

至于为何,请听我解释:在内重循环枚举背包容量结束后,你所需要的只有前i个物品的最优解已经出来了,并且存在dp数组中(注意当前的dp数组是一位数组),下一次外重循环便会加1,表示当前有i+1个物品供你选择,你需要得出最优解,显而易见,数组元素在没有被赋值前,会保持原来的那个值不变,而原来的那个值是什么呢?就是在只有前i个物品且质量为j的最优解,而你在用完这个值后,他就不会再出现在以后的计算中了,我们便可以用dp[j]=max(dp[[j],dp[j-v[i]]+w[i])的这个值来替换掉他。

代码如下:

#include<iostream>
#include<iomanip>
using namespace std;
const int MAXN=1010;
int dp[MAXN],n,m,v[MAXN],w[MAXN];
int main(){
	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=m;j>=v[i];j--){
			dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
		}
	}
	cout<<dp[m]<<endl;
	return 0;
}

这便是01背包问题的滚动数组优化。

到目前为止,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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值