首页 > 代码库 > 动态规划之背包问题

动态规划之背包问题

动态规划的基本思想:

将一个问题分解为子问题递归求解,且将中间结果保存以避免重复计算。通常用来求最优解,且最优解的局部也是最优的。求解过程产生多个决策序列,下一步总是依赖上一步的结果,自底向上的求解。

动态规划算法可分解成从先到后的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背包问题的思路:将一种物品拆成多件物品。