01背包问题
N件物品和一个容量为V的背包,第i个物品的价值为
w[i]
,体积为
c[i]
,求解哪些物品装入背包价值最大。
0-1背包:没见物品只有一个,要么装入要么不装入。
Solution 1 二维数组
dp问题,定义递归式
opt[i][j]
为前i个物品在容量为j的情况下的最大装载价值。
对于第i个物品来说,它只有两种选择:装入或者不装入。定义递归式:
opt[i−1][j] 表示第i件物品不装入背包,问题转化为前i-1件物品放入容量为j的包中
opt[i−1][j−c[i]]+w[i] 表示第i件物品装入背包,问题转化为前i-1件物品放入j-c[i]容量的包中
输入样例:
第一行两个数,第一个数表示背包容量,第二个数表示物品的数量N。
后面N行分别每一行分别表示一个物品的体积及其价值。
100 4
71 5
23 1
22 2
10 2
int max(int a, int b)
{
return (a > b) ? a : b;
}
void main()
{
int t, m;
cin >> t >> m;
int cost[101] = { 0 };
int val[101] = { 0 };
int dp[101][1001] = { 0 };//行列都多一位,防止i-1无效
int i, j;
for ( i = 1; i <= m; i++)
cin >> cost[i] >> val[i];
for (i = 1; i <= m;i++)
for (j = 1; j <= t; j++)//dp[i][j]表示前i个物品在j的耗时下最大收益
{
if (j >= cost[i])//对商品i能否放下
{
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - cost[i]] + val[i]);
}
else
dp[i][j] = dp[i - 1][j];
}
cout << "Max value is " << dp[m][t] << endl;
//打印放置了哪些物品
//if dp[i][j]>dp[i-1][j],说明将物品i放入袋中
//下一步回溯时,i--, j = j-cost[i]
j = t;
for (i = m; i > 0; i--)
{
if (dp[i][j] > dp[i - 1][j])
{
cout << cost[i] << " " << val[i] << endl;
j = j - cost[i];
}
}
}
Solution 2 一维数组解法
以上方法的时间和空间复杂度均为O(VN),其中时间复杂度应该已经不能再优化了,但空间复杂度却可以优化到O(V )。
先考虑上面讲的基本思路如何实现,肯定是有一个主循环i = 1..N,每次算出来二维数组F [i, 0..V ]的所有值。那么,如果只用一个数组F [0..V ],能不能保证第i次循环结束后F [v]中表示的就是我们定义的状态F [i,v]呢?
F [i,v]是由F [i − 1,v]和F [i − 1,v − Ci]两个子问题递推而来,能否保证在推F [i,v]时(也即在第i次主循环中推F [v]时)能够取用F [i − 1,v]和F [i − 1,v − Ci]的值呢?事实上,这要求在每次主循环中我们以v = V..0的递减顺序计算F [v],这样才能保证推F [v]时F [v − Ci]保存的是状态F [i − 1,v − Ci]的值。伪代码如下:
F [0..V ] = 0
for i = 1 to N
for v = V to Ci
F [v] = max{F [v],F [v − Ci] + Wi}
其中的F [v] = max{F [v],F [v − Ci] + Wi}一句,恰就对应于我们原来的转移方
程,因为现在的F [v − Ci]就相当于原来的F [i − 1,v − Ci]。如果将v的循环顺序
从上面的逆序改成顺序的话,那么则成了F [i,v]由F [i,v − Ci]推导得到,与本题
意不符。
#include<iostream>
using namespace std;
int max(int a, int b)
{
return (a > b) ? a : b;
}
int main()
{
int t, m;
cin >> t >> m;
int cost[101] = { 0 };
int val[101] = { 0 };
int dp[1001] = { 0 };
int i,j;
for (i = 1; i <= m; i++)
cin >> cost[i] >> val[i];
for (i = 1; i <= m; i++)
for (j = t; j >= cost[i]; j--)
{
dp[j] = max(dp[j], dp[j - cost[i]] + val[i]);
}
cout << "max value is " << dp[t] << endl;
return 0;
}
完全背包问题
Problem
有N种物品和一个容量为V 的背包,每种物品都有无限件可用。放入第i种物品的耗费的空间是 Ci ,得到的价值是 Wi 。求解:将哪些物品装入背包,可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。
Solution
O(VN)的算法。
F [0..V ] = 0
for i = 1 to N
for v = Ci to V
F [v] = max(F [v], F [v − Ci] + Wi)
这个伪代码与01背包问题的伪代码只有v的循环次序不同而已。
为什么这个算法就可行呢?首先想想为什么01背包中要按照v递减的次序来循环。让v递减是为了保证第i次循环中的状态F [i, v]是由状态F [i − 1, v − Ci]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝无已经选入第i件物品的子结果F [i −1, v − Ci]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选入第i种物品的子结果F [i, v − Ci],所以就可以并且必须采用v递增的顺序循环。这就是这个简单的程序为何成立的道理。
状态转移方程:
将这个方程用一维数组实现,便得到了上面的伪代码。
最后抽象出处理一件完全背包类物品的过程伪代码:
def CompletePack(F, C, W )
for v = C to V
F [v] = max{F [v], f[v − C] + W}
Source code
#include <iostream>
using namespace std;
int max(int a, int b)
{
return (a > b) ? a : b;
}
int main()
{
int t, m;
cin >> t >> m;
int cost[101] = { 0 };
int val[101] = { 0 };
int dp[1001] = { 0 };
int i, j;
for (i = 1; i <= m; i++)
cin >> cost[i] >> val[i];
for (i = 1; i <= m; i++)
for (j = cost[i]; j <= t; j++)
{
dp[j] = max(dp[j], dp[j - cost[i]] + val[i]);
}
cout << "The max value is " << dp[t];
return 0;
}
多重背包问题
当每种物品不止一件时,我们也可以将其转化为01背包问题。当物品有多件时,将每一件物品看成一个类别,在预处理时做相应转化即可处理。
Input:输入数据首先包含一个正整数C,表示有C组测试用例,每组测试用例的第一行是两个整数n和m(1<=n<=100, 1<=m<=100),分别表示经费的金额和大米的种类,然后是m行数据,每行包含3个数p,h和c(1<=p<=20,1<=h<=200,1<=c<=20),分别表示每袋的价格、每袋的重量以及对应种类大米的袋数。
Output:
对于每组测试数据,请输出能够购买大米的最多重量,你可以假设经费买不光所有的大米,并且经费你可以不用完。每个实例的输出占一行。
#include<iostream>
using namespace std;
int max(int a, int b)
{
if (a > b)
return a;
return b;
}
int main()
{
int times;
cin >> times;
while (times-- > 0)
{
int money = 0, kind = 0;
cin >> money >> kind;
int cost[2001] = { 0 };
int val[2001] = { 0 };
int dp[2001][101] = { 0 };
int k = 1;
for (int i = 0; i < kind; i++)
{
int h, p, c;
cin >> h >> p >> c;
for (int j = 0; j < c; j++)//每一件物品看成一个类别
{
cost[k] = h;
val[k] = p;
k++;
}
}
for (int i = 1; i < k; i++)
for (int j = 1; j <= money; j++)
{
if (cost[i] > j)
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j - cost[i]] + val[i], dp[i-1][j]);
}
cout << dp[--k][money] << endl;
}
return 0;
}
方案总数
设背包容量为V,一共N件物品,每件物品体积为C[i],每件物品的价值为W[i]
1) 子问题定义:F[i][j]表示前i件物品中选取若干件物品放入剩余空间为j的背包中所能得到的最大价值。
2) 根据第i件物品放或不放进行决策。
最优方案总数这里指物品总价值最大的方案数。
我们设G[i][j]代表F[i][j]的方案总数,那么最终结果应该是G[N][V]。我们初始化G[][]为1,因为对每个F[i][j]至少应该有一种方案,即前i件物品中选取若干件物品放入剩余空间为j的背包使其价值最大的方案数至少为1,因为F[i][j]一定存在。
下面开始分析怎么求G[i][j]。对于01背包来说:
如果F[i][j]=F[i-1][j]且F[i][j]!=F[i-1][j-C[i]]+W[i]说明在状态[i][j]时只有前i-1件物品的放入才会使价值最大,所以第i件物品不放入,那么到状态[i][j]的方案数应该等于[i-1][j]状态的方案数即G[i][j]=G[i-1][j];
如果F[i][j]=F[i-1][j-C[i]]+W[i] 且F[i][j]!=F[i-1][j]说明在状态[i][j]时只有第i件物品的加入才会使总价值最大,那么方案数应该等于[i-1][j-C[i]]的方案数,即G[i][j]=G[i-1][j-C[i]];
如果F[i][j]=F[i-1][j-C[i]]+W[i] 且F[i][j]=F[i-1][j]则说明即可以通过状态[i-1][j]在不加入第i件物品情况下到达状态[i][j],又可以通过状态[i-1][j-C[i]]在加入第i件物品的情况下到达状态[i][j],并且这两种情况都使得价值最大且这两种情况是互斥的,所以方案总数为G[i][j]=G[i-1][j-C[i]]+ G[i-1][j]。
对于这类改变问法的问题,一般只需将状态转移方程中的max改成sum即可。例如若每件物品均是完全背包中的物品,转移方程即为
初始条件是F [0, 0] = 1。
事实上,这样做可行的原因在于状态转移方程已经考察了所有可能的背包组成方案。
例题详解
钱币兑换问题:有无穷张面值为1,5,10,20,50,100元的纸币,求兑换100元的方案总数。
问题分析:无穷背包求方案总数的问题。设初始状态F(0,0)=1,
转移方程
第i种纸币在容量为v的方案总数为:1.不放第i种纸币时容量为v的方案总数G[i-1,v];2.放第i中纸币时容量为 v−ci 的方案总数。
Source code
#include <iostream>
using namespace std;
int main()
{
int t, i,j;
cin >> t;
int cost[6] = { 1, 5, 10, 20, 50, 100 };
int dp[1001] = { 0 };
dp[0] = 1;//inital state
for (i = 0; i <= 5; i++)
for (j = cost[i]; j <= t; j++)
dp[j] = dp[j] + dp[j - cost[i]];
cout << dp[t] << endl;
return 0;
}