首页 > 代码库 > 动态规划之背包问题
动态规划之背包问题
动态规划的基本思想:
将一个问题分解为子问题递归求解,且将中间结果保存以避免重复计算。通常用来求最优解,且最优解的局部也是最优的。求解过程产生多个决策序列,下一步总是依赖上一步的结果,自底向上的求解。
动态规划算法可分解成从先到后的4个步骤:
1. 描述一个最优解的结构,寻找子问题,对问题进行划分。
2. 定义状态。往往将和子问题相关的各个变量的一组取值定义为一个状态。某个状态的值就是这个子问题的解(若有k个变量,一般用K维的数组存储各个状态下的解,并可根据这个数组记录打印求解过程。)。
3. 找出状态转移方程。一般是从一个状态到另一个状态时变量值改变。
4. 以“自底向上”的方式计算最优解的值。
5. 从已计算的信息中构建出最优解的路径。(最优解是问题达到最优值的一组解)
其中步骤1~4是动态规划求解问题的基础,如果题目只要求最优解的值,则步骤5可以省略。
背包问题
01背包: 有N件物品和一个重量为M的背包。(每种物品均只有一件)第i件物品的重量是w[i],价值是p[i]。求解将哪些物品装入背包可使价值总和最大。
完全背包: 有N种物品和一个重量为M的背包,每种物品都有无限件可用。第i种物品的重量是w[i],价值是p[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包重量,且价值总和最大。
多重背包: 有N种物品和一个重量为M的背包。第i种物品最多有n[i]件可用,每件重量是w[i],价值是p[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包重量,且价值总和最大。
01背包问题:
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
用子问题定义状态:即c[i][m]表示前i件物品恰放入一个重量为m的背包可以获得的最大价值。则其状态转移方程便是:
c[i][m]=max{c[i-1][m],c[i-1][m-w[i]]+p[i]}
这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前i件物品放入重量为m的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,价值为c[i-1][m];如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的重量为m-w[i]的背包中”,此时能获得的最大价值就是c[i-1][m-w[i]]再加上通过放入第i件物品获得的价值p[i]。
测试数据:
10,3
3,4
4,5
5,6
c[i][j]数组保存了1,2,3号物品依次选择后的最大价值.
这个最大价值是怎么得来的呢?从背包容量为0开始,1号物品先试,0,1,2,的容量都不能放.所以置0,背包容量为3则里面放4.这样,这一排背包容量为4,5,6,....10的时候,最佳方案都是放4.假如1号物品放入背包.则再看2号物品.当背包容量为3的时候,最佳方案还是上一排的最价方案c为4.而背包容量为4的时候,则最佳方案为自己的重量5.背包容量为7的时候,很显然是5加上一个值了。加谁??很显然是7-4=3的时候.上一排c3的最佳方案是4.所以。总的最佳方案是5+4为9.这样.一排一排推下去。最右下放的数据就是最大的价值了。(注意第3排的背包容量为7的时候,最佳方案不是本身的6.而是上一排的9.说明这时候3号物品没有被选.选的是1,2号物品.所以得9.)
#include <iostream>
using namespace std;
const int MAXN = 100;
const int MAXM = 1000;
/**
* 01背包
* @param n 物品个数
* @param m 背包承重量
* @param p[] 物品价值
* @param w[] 物品重量
* @return 最大价值
*/
int knapsack01(int n, int m, int p[], int w[])
{
int c[MAXN][MAXM];
for(int i=1; i<=n; ++i)
{
c[i][0] = 0;
}
for(int i=1; i<=m; ++i)
{
c[0][i] = 0;
}
for(int i=1; i<=n; ++i)
{
for(int j=1; j<=m; ++j)
{
if((w[i]<=j) && (c[i-1][j] < c[i-1][j-w[i]]+p[i]))
{
c[i][j] = c[i-1][j-w[i]]+p[i];
}
else
{
c[i][j] = c[i-1][j];
}
}
}
return c[n][m];
}
int main(int argc, char** argv)
{
int p[MAXN];
int w[MAXM];
p[1] = 4;
p[2] = 5;
p[3] = 6;
w[1] = 3;
w[2] = 4;
w[3] = 5;
cout << knapsack01(3,10,p,w) <<endl;
return 0;
}
这里是用二位数组存储的,可以把空间优化,用一位数组存储。
用f[0..v]表示,f[v]表示把前i件物品放入容量为v的背包里得到的价值。把i从1~n(n件)循环后,最后f[v]表示所求最大值。
*这里f[v]就相当于二位数组的f[i][v]。那么,如何得到f[i-1][v]和f[i-1][v-w[i]]+p[i]?
首先要知道,我们是通过i从1到n的循环来依次表示前i件物品存入的状态。即:for i=1..N 现在思考如何能在是f[v]表示当前状态是容量为v的背包所得价值,而又使f[v]和f[v-c[i]]+w[i]标签前一状态的价值?
逆序!
for i=1..N
for v=V..0
f[v]=max{f[v],f[v-w[i]]+p[i]};
这里为什么要逆序呢?想想max中比较的两部分,对于f[v],当前i情况下的f[v]值还没有求出来,所以f[v]中存储的必然是i-1情况下的值,即为原来的f[i-1][v];但是f[v-w[i]]就不同了,如果是正序,当前i情况下,在求f[v]之前,很可能f[v-w[i]]已经计算出值了,所以为了保证f[v-w[i]] = f[i-1][v-c[i]],需要逆序,确保当前i的情况下,比v小的值还没有计算出来。
完全背包
这个问题非常类似于01背包问题,所不同的是每种物品有无限件,也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……取M/w[i]件等很多种。如果仍然按照解01背包时的思路,令c[i][m]表示前i种物品放入一个容量为m的背包的最大价值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:
c[i][m]=max{c[i-1][m],c[i-1][m-k*w[i]]+k*p[i]}(0<=k*w[i]<=m)
这跟01背包问题一样有O(N*M)个状态需要求解,但求解每个状态c[i][m]的时间是O(m/w[i]),总的复杂度是超过O(NM)的。
#include <iostream>
using namespace std;
const int MAXN = 100;
const int MAXM = 1000;
/**
* 完全背包
* @param n 物品个数
* @param m 背包承重量
* @param p[] 物品价值
* @param w[] 物品重量
* @return 最大价值
*/
int entirelyKnapsack(int n, int m, int p[], int w[])
{
int c[MAXN][MAXM];
for(int i=1; i<=n; ++i)
{
c[i][0] = 0;
}
for(int i=1; i<=m; ++i)
{
c[0][i] = 0;
}
for(int i=1; i<=n; ++i)
{
for(int j=1; j<=m; ++j)
{
if(w[i]<=j)
{
if(c[i-1][j] < c[i-1][j-w[i]]+p[i])
{
c[i][j] = c[i-1][j-w[i]]+p[i];
}
else
{
c[i][j] = c[i-1][j];
}
//多个的情况
if(c[i][j] < c[i][j-w[i]]+p[i])
{
c[i][j] = c[i][j-w[i]]+p[i];
}
}
else
{
c[i][j] = c[i-1][j];
}
}
}
return c[n][m];
}
int main(int argc, char** argv)
{
int p[MAXN];
int w[MAXM];
p[1] = 4;
p[2] = 5;
p[3] = 6;
w[1] = 3;
w[2] = 4;
w[3] = 5;
//cout << knapsack01(3,10,p,w) <<endl;
cout << entirelyKnapsack(3,10,p,w) <<endl;
return 0;
}
将01背包问题的基本思路加以改进,得到了这样一个清晰的方法。这说明01背包问题的状态转移方程可以推及其它类型的背包问题。但是由于复杂度太高,我们还是试图改进这个复杂度。
完全背包问题有一个很简单有效的优化,是这样的:若两件物品i、j满足p[i]<=p[j]且w[i]>=w[j],则将物品i去掉,不用考虑。这个优化的正确性显然:任何情况下都可将重量大费用高得i换成物美价廉的j,得到至少不会更差的方案。对于随机生成的数据,这个方法往往会大大减少物品的件数,从而加快速度。然而这个并不能改善最坏情况的复杂度,因为有可能特别设计的数据可以一件物品也去不掉。这个优化可以简单的O(N^2)地实现,一般都可以承受。
另外,针对背包问题而言,比较不错的一种方法是:首先将费用大于M的物品去掉,然后使用类似计数排序的做法,计算出费用相同的物品中价值最高的是哪个,可以O(M+N)地完成这个优化。
既然01背包问题是最基本的背包问题,那么我们可以考虑把完全背包问题转化为01背包问题来解。最简单的想法是,考虑到第i种物品最多选M/w[i]件,于是可以把第i种物品转化为M/w[i]件价值为p[i]及重量为w[i]的物品,然后求解这个01背包问题。这样完全没有改进基本思路的时间复杂度,但这毕竟给了我们将完全背包问题转化为01背包问题的思路:将一种物品拆成多件物品。