本小结介绍0-1背包、完全背包以及多重背包问题
记忆要点:
0-1背包:二维数组情况下,顺序遍历体积或者倒序均可以
降维情况下需倒序遍历体积
完全背包:数组降维+顺序遍历
多重背包:进行类似于二进制分解的操作,然后转化为0-1背包求解
背包问题变种一: 恰好装满指定的体积 解决思路为初始化时先将dp[ j ]初始化为不存在,而dp[0]初始化为0,遍历过程中先判断前提条件存不存在。存在与否可用额外的布尔数组界定或者加入一个if判断语句。
0-1背包
采药(九度教程第 101 题)
时间限制:1 秒 内存限制:32 兆 特殊判题:否
题目描述:
辰辰是个很有潜能、天资聪颖的孩子,他的梦想是称为世界上最伟大的医师。
为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个
难题。医师把他带到个到处都是草药的山洞里对他说:“孩子,这个山洞里有一
些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你
一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你
应该可以让采到的草药的总价值最大。”如果你是辰辰,你能完成这个任务吗?
输入:
输入的第一行有两个整数 T(1 <= T <= 1000)和 M(1 <= M <= 100),T 代
表总共能够用来采药的时间,M 代表山洞里的草药的数目。
接下来的 M 行每行包括两个在 1 到 100 之间(包括 1 和 100)的的整数,
分别表示采摘某株草药的时间和这株草药的价值。
输出:
可能有多组测试数据,对于每组数据,
输出只包括一行,这一行只包含一个整数,表示在规定的时间内,可以采到
的草药的最大总价值。
样例输入:
70 3
71 100
69 1
1 2
样例输出:
3
问题抽象:有一个容积V的背包和一些物品。这些物品分别有价值v和体积w这两种属性,每种物品只有一个,求背包中能够包含的最大价值,背包可以不装满。
关键词:每种物品只有一个、背包可以不装满
状态: 用dp[ i ][ j ]表示在总体积不超过j的情况下,前i个物品所能达到的最大价值。
初始时,dp[0][ j ] = 0 (0<=j<=V)
状态转移: 根据每种物品是否被放入背包来进行
状态转移方程式:以第i种物品为例,设价值为v,代价为w
dp[i][j] = max(dp[i-1][j-w]+v,dp[i-1][j]);
在上述方程式中,i>=1,且已经对dp[0][ j ]的情况进行过初始化,因此要考虑j-w与0的关系,若小于0则该转移来源不能被转移。
代码:
//0-1背包, 动态规划
#include<iostream>
#include<algorithm>
using namespace std;int T, M;
int dp[101][1001];//output: dp[M][T]
int cost[101];
int value[101];
int main() {
cin >> T >> M;for (int i = 1; i <= M; i++) {
cin >> cost[i] >> value[i];}for (int i = 0; i <= T; i++) {
dp[0][i] = 0; }for (int i = 0; i <= M; i++) {
dp[i][0] = 0; }for (int i = 1; i <= M; i++) {
for (int j = 1; j <= T; j++) {
if (j - cost[i] >= 0) {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - cost[i]] + value[i]);}else dp[i][j] = dp[i - 1][j];//此时只能有一种情况}}cout << dp[M][T] << endl;//system("pause");
}
观察状态转移的特点,我们发现dp[i][ j ]的转移仅与dp[i-1][j-cost[i]]和dp[i-1][ j ]有关,即仅与二维数组中本行的上一行有关。根据该特点,可以将对数组进行降维,将原本的二维数组优化为一维数组,状态转移方程式如下:
dp[j] = max(dp[j-cost[i]]+v,dp[j]);
其中在本次更新中未经修改的 dp[ j-cost[i]]与 dp[ j ]与原始写法中的
dp[i-1][ j-cost[i]]与 dp[i-1][ j ]等值。为了保证状态正确的转移,我们必须保证在
每次更新中确定状态 dp[ j ]时,dp[ j ]和 dp[ j-cost[i]]尚未被本次更新修改。考虑到
j - list[i].w < j,那么在每次更新中 倒序遍历 所有 j 的值,就能保证在确定 dp[ j ]的新
值时,dp[ j-cost[i]]的值尚未被修改,从而完成正确的状态转移。
代码:
#include<iostream>
#include<algorithm>
using namespace std;int T, M;
int dp[1001];
int cost[101];
int value[101];
int main() {
cin >> T >> M;for (int i = 1; i <= M; i++) {
cin >> cost[i] >> value[i];}for (int i = 0; i < 1001; i++) {
dp[0] = 0; }for (int i = 1; i <= M; i++) {
for (int j = T; j >= cost[i]; j--) {
dp[j] = max(dp[j], dp[j - cost[i]] + value[i]);}}cout << dp[T] << endl;//system("pause");
}
关键点:数组降维后要倒序遍历
分析求解 0-1 背包问题的算法复杂度:其状态数量为 M*T,其中 M 为物品数
量,T为背包的总容积,状态转移复杂度为 O(1),所以综合时间复杂度为 O(M*T)。
经优化过后的空间复杂度仅为 O(T)(不包括保存物品信息所用的空间)。
0-1背包是最基本的背包问题,它的特点是每种物品最多只能选择一件
0-1背包变种,即恰好装满背包
0-1背包存在一个简单的变化,即要求选择的物品必须恰好装满背包,此时我们用状态dp[i][j]
表示在总体积恰好为j的情况下,前i种物品所能达到的最大价值。其状态转移与前文中所讲的0-1背包完全一致,而初始状态发生变化。dp[0][0]为0,而其它dp[0][ j ]不存在,这样做的目的是只有dp[i-1][ j - cost[i]]存在时(即可以组成体积 j - cost[i]时),再加上物品i,才可能组成dp[i][ j ]. dp[M][V]存在时即为答案,否则无法组成背包的总体积V
该种类型的题自然也可以进行数组降维。
不存在可由一个bool数组或者if(dp[ j - cost[i]] == -INF)来判断。
关键点:初始化时有所不同,要先将dp[ j ]初始化为不存在,而dp[0]初始化为0
这种变化的例题在下面介绍完全背包时给出。
完全背包
对0-1背包问题进行扩展,使得每种物品的数量无限增加,便得到完全背包问题:有一个容积为V的背包,同时有n种物品,每种物品有其各自的体积w和价值v,每种物品的数量均为无限个,求背包中能够装下的最大价值。
核心代码:
for (int i = 1; i <= n; i++) {
for (int j = list[i].w; j <= V; j++) {
dp[j] = max(dp[j], dp[j - list[i].w] + list[i].v);}
}
注意到与前面提到的0-1背包数组降维需要 倒序遍历 的情况不同,解决完全背包问题时需要 顺序遍历 ,原因如下:
在0-1背包中,倒序遍历是为了保证更新dp[ j ]时,dp[ j - list[i].w]是没有放入物品i时的数据(可视为dp[i-1][ j -list[i].w]),这是因为0-1背包中每个物品最多被选中一次。但在完全背包中,每个物品可以被无限次选择,那么状态 dp[i][ j ]恰好可以由可能已经放入物品 i 的状态 dp[i][ j - list[i].w]转移而来,固在这里将状态的遍历顺序改为顺序,使在更新状态 dp [ j ]时,dp[ j - list[i].w]可能因为放入物品 i 而发生改变,从而达到目的。
下面是一道基于完全背包的例题(POJ 1384 ):
Piggy-Bank
Description
Before ACM can do anything, a budget must be prepared and the necessary financial support obtained. The main income for this action comes from Irreversibly Bound Money (IBM). The idea behind is simple. Whenever some ACM member has any small money, he takes all the coins and throws them into a piggy-bank. You know that this process is irreversible, the coins cannot be removed without breaking the pig. After a sufficiently long time, there should be enough cash in the piggy-bank to pay everything that needs to be paid.
But there is a big problem with piggy-banks. It is not possible to determine how much money is inside. So we might break the pig into pieces only to find out that there is not enough money. Clearly, we want to avoid this unpleasant situation. The only possibility is to weigh the piggy-bank and try to guess how many coins are inside. Assume that we are able to determine the weight of the pig exactly and that we know the weights of all coins of a given currency. Then there is some minimum amount of money in the piggy-bank that we can guarantee. Your task is to find out this worst case and determine the minimum amount of cash inside the piggy-bank. We need your help. No more prematurely broken pigs!
Input
The input consists of T test cases. The number of them (T) is given on the first line of the input file. Each test case begins with a line containing two integers E and F. They indicate the weight of an empty pig and of the pig filled with coins. Both weights are given in grams. No pig will weigh more than 10 kg, that means 1 <= E <= F <= 10000. On the second line of each test case, there is an integer number N (1 <= N <= 500) that gives the number of various coins used in the given currency. Following this are exactly N lines, each specifying one coin type. These lines contain two integers each, Pand W (1 <= P <= 50000, 1 <= W <=10000). P is the value of the coin in monetary units, W is it’s weight in grams.
Output
Print exactly one line of output for each test case. The line must contain the sentence “The minimum amount of money in the piggy-bank is X.” where X is the minimum amount of money that can be achieved using coins with the given total weight. If the weight cannot be reached exactly, print a line “This is impossible.”.
Sample Input
3
10 110
2
1 1
30 50
10 110
2
1 1
50 30
1 6
2
10 3
20 4
Sample Output
The minimum amount of money in the piggy-bank is 60.
The minimum amount of money in the piggy-bank is 100.
This is impossible.
题目大意:
有一个存钱罐,告诉其空时的重量和当前重量,并给出一些硬币的价值和重量,每种硬币数量不限,求出存钱罐中最少有多少钱。
解题思路:
在本题中,完全背包有两处变化,其一是求的是最小值而非最大值,这就需要我们在状态转移时保存较小值;其二是要求重量正好为给出的数值,即在背包问题中表现为恰好装满,在前面的讨论中我们已经知道这需要对初始值进行改变。
具体分析见代码注释:
AC代码:
#include<iostream>
#include<algorithm>
using namespace std;
#define INF 0x7fffffffstruct {
int w;//weightint v;//value
}list[501];
int dp[10005];
int main(){
int t;cin >> t;int E, F, num;while (t--) {
cin >> E >> F;cin >> num;//硬币种类数for (int i = 1; i <= num; i++) {
cin >> list[i].v >> list[i].w;}dp[E] = 0;//dp[E]相当于是起点dp[0]for (int i = E+1; i <= F; i++) {
dp[i] = INF;//在这里我们使用一个特殊值来表示不存在}for (int i = 1; i <= num; i++) {
for (int j = E + list[i].w; j <= F; j++) {
if (dp[j - list[i].w] != INF) {
//前提条件存在dp[j] = min(dp[j], dp[j - list[i].w] + list[i].v);}}}if(dp[F] != INF){
//能够正好装满背包cout << "The minimum amount of money in the piggy-bank is ";cout<< dp[F] << "." << endl;}else cout << "This is impossible." << endl;}return 0;
}
多重背包
多重背包介于0-1背包和完全背包之间,每种物品的数量不再是无穷或者都是1,而是某一个确定的值k,可以将多重背包问题转化到0-1背包问题上去: 将原数量为k的物品进行二进制拆分,进而拆分成若干组,每组物品视为一件物品,一同参与选择过程,每组物品包含的原物品个数为1、2、4…k-2c+1,其中c是使 k-2c+1大于0的最大整数,(最后一项的个数为k-1-2-4-…-2c-1,对等比数列求和可化为上述形式)。这种类似于二进制的拆分,不仅将物品数量大大降低,同时通过对这些新得到的若干物品进行组合,可以得到0到k之间的任意物品的价值和重量和,因此对这些新物品做0-1背包,即可得到多重背包的解。由于转化后的 0-1 背包物品数量大大降低,其时间复杂度也得到较大优化,为:
O ( V ? ∑ i = 1 n l o g 2 ( k i ) ) O(V*\sum_{i=1}^n log_2(k_i)) O(V?i=1∑n?log2?(ki?))
珍惜现在,感恩生活(九度教程第 103 题)
时间限制:1 秒 内存限制:32 兆 特殊判题:否
题目描述:
为了挽救灾区同胞的生命,心系灾区同胞的你准备自己采购一些粮食支援灾
区,现在假设你一共有资金 n 元,而市场有 m 种大米,每种大米都是袋装产品,
其价格不等,并且只能整袋购买。请问:你用有限的资金最多能采购多少公斤粮
食呢?
输入:
输入数据首先包含一个正整数 C,表示有 C 组测试用例,每组测试用例的第
一行是两个整数 n 和 m(1<=n<=100, 1<=m<=100),分别表示经费的金额和大米的
种 类 , 然 后 是 m 行 数 据 , 每 行 包 含 3 个 数 p , h 和
c(1<=p<=20,1<=h<=200,1<=c<=20),分别表示每袋的价格、每袋的重量以及对应
种类大米的袋数。
输出:
对于每组测试数据,请输出能够购买大米的最多重量,你可以假设经费买不
光所有的大米,并且经费你可以不用完。每个实例的输出占一行。
样例输入:
1
8 2
2 100 4
4 100 2
样例输出:
400
AC代码:
#include<iostream>
#include<algorithm>
using namespace std;int C;
int dp[200000][101];
struct E {
int v;int w;
};
E list[2001];
int main() {
cin >> C;int m, n;int p, h, c;while (C--) {
cin >> n >> m;int time = 1;for (int i = 1; i <= m; i++) {
cin >> p >> h >> c;int tmp = 1;while (c - tmp >= 0) {
//二进制拆分list[time].v = tmp *h;list[time].w = tmp *p;time++;c -= tmp;tmp *= 2;}if (c > 0) {
list[time].v = c *h;list[time].w = c *p;time++;}}//time-1是拆分后的种类数for (int i = 0; i <= n; i++) {
dp[0][i] = 0; }for (int i = 0; i < time; i++) {
dp[i][0] = 0; }for (int i = 1; i < time; i++) {
for (int j = 1; j <= n; j++) {
if (j >= list[i].w) {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - list[i].w] + list[i].v);}else dp[i][j] = dp[i - 1][j];}}cout << dp[time - 1][n] << endl;}//system("pause");
}
当然这一题也是可以进行数组降维的。
(?3」∠) 写到这里了发现书中给的0-1背包题解在二维数组情况下对体积也是倒序遍历的,不过此时是二维数组,数据依据上一行进行更新,因此也不会产生物品取1次以上的情况,顺序遍历也可以。