01背包和完全背包

01背包

问题描述

n n n 件物品和一个容量为 V V V 的背包。第i件物品的体积是 v i v_i vi,价值是 c i c_i ci。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。

数据规模

n ≤ 100 n \le 100 n100
1 ≤ v i , c i ≤ 100 1 \le v_i, c_i \le 100 1vi,ci100
V ≤ 10000 V \le 10000 V10000

样例输入

n = 4 n = 4 n=4
( v , c ) = { ( 2 , 3 ) , ( 1 , 2 ) , ( 3 , 4 ) , ( 2 , 2 ) } (v, c) = \{ (2, 3), (1, 2), (3, 4), (2, 2) \} (v,c)={(2,3),(1,2),(3,4),(2,2)}
V = 5 V = 5 V=5

样例输出

7 7 7

问题求解

n n n 件物品,每一件都可以选或不选,所以最直接的想法就是搜索,结束条件是体积小于等于0,这样算法的时间复杂度就是 O ( 2 n ) O(2^n) O(2n),以这题的数据规模来说肯定是不行的,所以要降复杂度。跟踪函数的每一次调用,可以发现,有很多的重复计算,在这种情况下,我们考虑动态规划求解,把已经得出的结果存下来,供后面计算使用。

现在想象有 V V V个背包,以 j ( 1 ≤ j ≤ V ) j(1 \le j \le V) j(1jV) 表示第 j j j 个背包,第 j j j 个背包的体积是 j j j ,我们将 n n n 个物品依次放入 V V V 个背包,所以需要一个二维数组,二维数组定义如下:

const int N = 5e3;
int dp[N][N];

定义了一个dp全局数组,数组里的每一项都初始化为0。dp[i][j]的意义是:对于第 i i i 件物品,第 j j j 个背包能获得的最大价值。

为了表示 v i v_i vi c i c_i ci,我们再定义两个数组。

const int N = 5e3;
int dp[N][N];
//v[i]表示第i件物品的体积,1 <= i <= n
//c[i]表示第i件物品的价值,1 <= i <= n
int v[N];
int c[N];

现在开始放第一个物品,对于第一件物品,我们将它依次放入第 j j j个背包,上面的样例输入第一件物品的体积是2,价值是3,即 v[1] = 2c[1] = 3,第1个背包的体积为1,1 < v[1],不能放在第1个背包,所以 dp[1][1]的值是0;第2个背包的体积为2,2 = v[1],可以放在第2个背包,dp[1][2]的值变成 c[1];从第三个背包开始都有 j > v[1],所以第1件物品可以放入从第三个背包开始的所有背包,所以有dp[1][j]=c[1]

i \ j012345
0000000
1003333

现在,我们已经记录下了放第1个物品时,每个背包容量所能达到的最大价值,假设现在我想知道背包的容量是3时,放第1个物品能获得多少价值呢?很简单,dp[1][3]就是答案。

放完第1个物品后,我们开始放第2个物品,v[2] = 1c[2] = 2,同样从第1个背包开始,第1个背包的体积为1,1 < v[2],可以放在第1个背包,但是放之前我们就要考虑了,在这里是放第2件物品获得的价值大,还是不放获得的价值大呢?就第2个物品,第1个背包而言,不放物品获得的价值我们已经在前面放第1个物品时求出来了,就是dp[1][1];那如果放第2个物品呢?要想放第2个物品,背包首先要给第二个物品腾出空,这样背包就只剩下1 - v[2]的空间来放其它物品了,再想一下,其他物品是什么呢?显然就是第2个物品前面的物品了,只有物品1,第一个物品的状态我们已经记录下来了,直接拿来用就行了,所以放完物品后剩余的空间所能获得的最大价值是dp[1][1-v[2]],加上第2件物品的价值就得到了将第2件物品放入第1个背包后能获得的价值,也就是dp[1][1-v[2]] + c[2]dp[1][1]dp[1][1-v[2]] + c[2]取最大值,即为放第2个物品时,第1个背包所能达到的最大价值。

讲到这里我们就要讲一下dp[0][j]dp[i][0]的意义了,看一下dp[1][1-v[1]] + c[2]1-v[1] = 1 - 1 = 0dp[1][0]是多少呢?前面说过dp是一个全局数组,初始化为0,所以dp[1][0] = 0,这合理吗?dp[1][0]代表什么意义呢?它表示容量为0的背包放第1件物品所能获得的最大价值,背包容量为0,什么也不能放,dp[1][0]=0合理,将1推广到 i i idp[i][0]=0合理。再来看 dp[0][j], 放第1个物品时,我们来看dp[1][2],就像放第2个物品时一样,我们会从dp[1][j]dp[1][j-v[2]] + c[2]中选取一个最大值(当然前提是j>=v[2],如果j<v[2],就只能放第1个物品了,也就是说只取dp[1][j]),放第1个物品时也会比较dp[0][j]dp[0][j-v[1]] + c[1]的值,这么比较有意义吗?首先看不放第一个物品时的情况:dp[0][j]=0,合理;再来看放第一个物品时的情况dp[0][j-v[1]] + c[1]=0+c[1]=c[1],合理。所以dp[0][j]dp[i][0]的值为0时初始条件,并且这个初始条件的设置是合理的。

继续放第2个物品,j = 2时,可以放第2个物品,比较dp[1][2]=3dp[1][2-v[2]] + c[2]=dp[1][1] + c[2]=0 + 2=2,所以我们选择不放第2个物品。如果选择放第2个物品,那么放完后剩余的体积所能获得的最大价值加上放第2个物品所获得的价值相加并不比不放第2个物品所获得的价值大。
接着放第3个物品,dp[1][3]=3dp[1][3-v[2]] + c[2]=dp[1][2] + c[2]=3 + 2=5,选择放第2个物品。因为放完第2个物品后剩余2容量,前面已经求出dp[1][2] = 3,所以第3个背包可以获得的最大价值是5。
同理,我们可以获得放第2个物品时,dp数组的值

i \ j012345
0000000
1003333
2025555

讲解到这,我们可以得出一个递推公式了:
d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] , j &lt; v[i] max ⁡ ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v [ i ] ] + c [ i ] ) , j &gt;= v[i] dp[i][j] = \begin{cases} dp[i-1][j], &amp; \text{j &lt; v[i]} \\ \max(dp[i - 1][j],dp[i - 1][j - v[i]] + c[i]), &amp; \text{j &gt;= v[i]} \end{cases} dp[i][j]={dp[i1][j],max(dp[i1][j]dp[i1][jv[i]]+c[i]),j < v[i]j >= v[i]
利用这个递推公式我们可以写出解决这个问题的程序:

#include <iostream>
#include <fstream>
#include <algorithm>
using namespace std;

const int N = 5e3;
int c[N];
int v[N];
int dp[N][N];
int n, V;

void solve() {
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= V; ++j) {
			if (j < v[i]) {
				dp[i][j] = dp[i - 1][j];
			}
			else {
				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + c[i]);
			}
		}
	}
}

int main(int argc, char** argv) {
	cin >> n;
	for (int i = 1; i <= n; ++i) {
		cin >> v[i] >> c[i];
	}
	cin >> V;
	solve();
	cout << dp[n][V] << endl;
	
	return 0;
}

时间复杂度为 O ( n W ) O(nW) O(nW),完全可以应对这一题的数据量。

完全背包

问题描述

n n n 件物品和一个容量为 V V V 的背包。第i件物品的体积是 v i v_i vi,价值是 c i c_i ci。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。在这里,可以重复选取同一个物品若干次。

数据规模

n ≤ 100 n \le 100 n100
1 ≤ v i , c i ≤ 100 1 \le v_i, c_i \le 100 1vi,ci100
V ≤ 10000 V \le 10000 V10000

问题求解

和01背包相比,完全背包可以重复选取同一个物品,理所当然的,我们可以枚举所有的情况,我们来讨论向第j个背包放第i个物品,首先看不放的情况,接着看放1个的情况(如果背包容量够的话),然后是第3个、第4个…
可以在01背包的基础上加一层循环实现(只给出关键代码,其余一样):

void solve() {
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= V; ++j) {
			for (int k = 0; k * v[i] <= j; ++k) {
				dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v[i]] + k * c[i]);
			}
		}
	}
}

最坏情况下,算法的复杂度能达到 O ( n M M ) O(nMM) O(nMM),就这题而言,达到了 1 0 10 10^ {10} 1010级,解决不了这一题。

所以还需要优化,观察有没有哪里重复计算了,我们以计算dp[3][5]为例(且不管输入是什么),假设v[3] = 2,那么,会从dp[2][5]dp[2][3] + c[3]dp[2][1] + 2 * c[3]中选一个最大值作为dp[3][5]的值。
接着看dp[3][7],会从dp[2][7]dp[2][5] + c[3]dp[2][3] + 2 * c[3]dp[2][1] + 3 * c[3]中选一个最大值,
可以看到求dp[3][7]时,抛去dp[2][7]不谈,它的选择方案和之前求dp[3][5]时选取的几组数每个都正好相差c[3],推广到全体,我们可以用如下递推关系求解:
d p [ i ] [ j ] = { d p [ i − 1 ] [ j ] , j &lt; v[i] max ⁡ ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − v [ i ] ] + c [ i ] ) , j &gt;= v[i] dp[i][j] = \begin{cases} dp[i-1][j], &amp; \text{j &lt; v[i]} \\ \max(dp[i - 1][j],dp[i][j - v[i]] + c[i]), &amp; \text{j &gt;= v[i]} \end{cases} dp[i][j]={dp[i1][j],max(dp[i1][j]dp[i][jv[i]]+c[i]),j < v[i]j >= v[i]
注意 j &gt; = v [ i ] j &gt;= v[i] j>=v[i] 时, d p [ i ] [ j − v [ i ] ] + c [ i ] dp[i][j - v[i]] + c[i] dp[i][jv[i]]+c[i] 中求的是dp[i][j - v[i]],也就是说会利用前面求将第 i 个物品放入 j - v[i] 时的记录值(这个前面已经推过了)。
根据这个递推式,我们可以写出如下程序:

#include <iostream>
#include <fstream>
#include <algorithm>
using namespace std;

const int N = 5e3;
int c[N];
int v[N];
int dp[N][N];
int n, V;

void solve() {
	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][j - v[i]] + c[i]);
			}
		}
	}
}

int main(int argc, char** argv) {
	cin >> n;
	for (int i = 1; i <= n; ++i) {
		cin >> v[i] >> c[i];
	}
	cin >> V;
	solve();
	cout << dp[n][V] << endl;
	
	return 0;
}

优化之后,算法的时间复杂度变为 O ( n M ) O(nM) O(nM),完全可以应对题目的数据量。

总结

动态规划是一种记录结果再利用的算法,观察01背包和完全背包的递推式,可以看到就只有 ii - 1 的区别,两种算法使用前面记录的数据不同,01背包只利用前面一组数据,而完全背包不只会利用前面一组数据,还会利用同一组数据。利用它们的这些特性还能使用一维数组替代二维数组。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值