动态规划之背包问题

让人想犯罪 __ 提交于 2019-12-15 13:13:53

背包问题是动态规划的一个分支,这里先简单介绍一下动态规划的思想。

动态规划程序设计是对解最优化问题的一种途径、一种方法,而不是一种特殊算法。不像搜索或数值计算那样,具有一个标准的数学表达式和明确清晰的解题方法。动态规划程序设计往往是针对一种最优化问题,由于各种问题的性质不同,确定最优解的条件也互不相同,因而动态规划的设计方法对不同的问题,有各具特色的解题方法,而不存在一种万能的动态规划算法,可以解决各类最优化问题。因此在学习时,除了要对基本概念和方法正确理解外,必需具体问题具体分析处理,以丰富的想象力去建立模型,用创造性的技巧去求解。我们也可以通过对若干有代表性的问题的动态规划算法进行分析、讨论,逐渐学会并掌握这一设计方法。(以上来自百度百科)

简单来说动态规划是一个思想,而不是一个固定的算法模板,我们需要通过这种思想确定状态转移方程(一个好的状态),然后再求解。

动态规划适用于很多情况,其中一些情况被统一划分归类,而背包问题则是一种最为常见的动态规划问题。

背包问题又分为许多种,如:01背包,完全背包等

我们这里介绍最为常见的三种,分别是01背包,完全背包,和多重背包。

1.01背包

这是一个经典的动态规划问题,另外在贪心算法里也有背包问题,至于二者的区别在此就不做介绍了。

题目一般都是有N件物品和一个容量为V的背包。第i件物品的体积是v[i],价值是c[i],将哪些物品装入背包可使价值总和最大?

分析起来很简单,每一种物品都有两种可能,即放入背包或者不放入背包。可以用dp[i][j]表示第i件物品放入容量为j的背包所得的最大价值,则状态转移方程可以推出如下:

dp[i][j]=max{dp[i-1][j-v[i]]+c[i],dp[i-1][j]};

先放下代码:

for (int i = 1;i <= N;i++) //枚举物品  
{  
    for (int j = 0;j <= V;j++) //枚举背包容量  
    {  
        f[i][j] = f[i - 1][j];
        if (j >= v[i])
        {  
            f[i][j] = Max(f[i - 1][j],f[i - 1][j - v[i]] + c[i]);
        }
    }}

 

我们可以发现0-1背包的状态转移方程 dp[i][j] = max{dp[i-1][j-w[i]]+v[i],dp[i-1][j]}的特点,当前状态仅依赖前一状态的剩余体积与当前物品体积v[i]的关系。根据这个特点,我们可以将dp降到一维即dp[j] = max{dp[j],dp[j-w[i]]+v[i]}。从这个方程中我们可以发现,有两个dp[j],但是要区分开。等号左边的dp[j]是当前i的状态,右边中括号内的dp[j]是第i-1状态下的值。

所以为了保证状态的正确转移,我们需要先更新等号左边中的dp[j](当前状态的dp[j])。

#include <iostream>
using namespace std;

#define MAXSIZE 100
int w[MAXSIZE];
int v[MAXSIZE];
int maxv;
int n;
int dp[MAXSIZE];

int max(int a, int b)
{
    if (a > b)
        return a;
    else
        return b;
}

int main()
{
    cin >> n >> maxv;
    for (int i = 1; i <= n; i++)
    {
        cin >> w[i] >> v[i];
    }
    for (int i = 0; i <= maxv; i++)
        dp[i] = 0;

    for (int i = 1; i <= n; i++)
    {
        //只有当j >= w[i],dp[j]才能进行选取最大值,否则dp[j]将不作更新,等于dp[i-1][j]。
        for (int j = maxv; j >= w[i]; j--)
        {
            dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
        }
        
    }

    cout << dp[maxv] << endl;
    return 0;
}

对于01背包问题,我们通常使用的是第二种方法,相比之第一种,第二种的空间复杂度无疑是减小了许多。

现在我们来看这个状态转移方程:dp[j] = max(dp[j], dp[j - w[i]] + v[i]);

我们先分析上一个状态也就是dp[j-w[i]],我们将上一个状态转移到现在的状态dp[j]有三种情况:

首先在现在的状态dp[j],在不转移的情况下这也是一种情况;

第二种是dp[j-w[i]],即当前物品不放入上一个状态;

第三种就是dp[j-w[i]]+v[i],即当前物品放入上一个状态;

状态转移的过程则是要求我们在这三者当中取最大值,我们知道v[i]必定是大于等于0的,也就是说dp[j-w[i]]+v[i]必定是大于等于dp[j-w[i]]的,我们可以把两者合并为一种情况,然后是dp[j],为什么dp[j]也算是一种情况,我们回到for循环可以发现第一个物品枚举完之后有一些状态dp[k*w[1]]更新了,它们的值都变成v[i],然后我们在接下来的过程当中dp[j]可能是已经有了值的,通过状态转移方程我们可以知道这个值是当物品总重量达到j时刻物品总价值的最大值。所以我们需要将dp[j]也看作是一种情况。

2.完全背包

对于01背包问题,它的特点是:每一件物品之多只能选择一件,即在背包中该物品数量只有0和1两种情况。

现在扩展一下,有一个容积为V的背包,同时有n种物品,每种物品均有无数多个,并且每种物品的都有自己的体积和价值。求使用该背包最多能够装的物品价值总和。

这就是完全背包问题。

如果按照0-1背包的思路求解该问题,可设当前物品的体积为w,价值为v,考虑到背包中最多存放V/w件该物品,那么该物品的可选数量就为V/w件。依次可以对所有的物品进行拆分,最后对拆分的所有物品做0-1背包即可得到答案。但是,这样拆分会使物品数量大大增加,其时间复杂度为:O(V*∑ni=1(V/wi))。

可见,当V较大时每个物品的体积较小时,其时间复杂度会显著增大。所以将完全背包问题转化为0-1背包问题去解决的方法不可靠。

在0-1背包的解决算法中,其中一段代码是该算法的核心算法,如下:

struct Good{
    int w;
    int v;
}goods[101];
int dp[101][1001];
int n,S;//n表示有n个物品,S表示背包的最大容积
for (i = 1; i <= n; i++)
{
    for (j = S; j >= goods[i].w; j--)
    {
         dp[j] = max(dp[j], dp[j - goods[i].w] + goods[i].v);
    }      
}

在这段代码中,之所将j初始化为S,逆序循环更新状态是为了保证在更新dp[j]时,dp[j-goods[i].w]的状态尚未因为本次更新而发生改变,即等价于由

dp[i-1][j-goods[i].w]转移得到dp[i][j]。保证了更新dp[j]时,dp[j-goods[i].w]是没有放入物品i时的数据dp[i-1][j-goods[i].w]。

在解决完全背包问题时,可以借鉴这个思路。在完全背包中,每个物品可以被无限次选择,那么状态dp[i][j]恰好可以由可能已经放入物品i的状态dp[i][j-goods[i].w]转移而来。可以将上面的代码改写如下:

for (i = 1; i <= n; i++)
{
    for (j = goods[i].w; j <= S; j++)
        dp[j] = max(dp[j], dp[j - goods[i].w] + goods[i].v);
}

这样不需要将物品拆分,但是本质上并没什么区别.

3.多层背包

多重背包问题是0-1背包问题和完全背包问题的综合体,可以描述如下:从n种物品向容积为V的背包装入,其中每种物品的体积为w,价值为v,数量为k,问装入的最大价值总和?

我们知道0-1背包问题是背包问题的基础,所以在解决多重背包问题的时候,要将多重背包向0-1背包上进行转换。在多重背包问题中,每种物品有k个,可以将每种物品看作k种,这样就可以使用0-1背包的算法。但是,这样会增加数据的规模。因为该算法的时间复杂度为O(V*∑ni=1ki),所以要降低每种物品的数量ki。

代码和完全背包的代码基本一样,唯一的不一样就是完全背包的每一种物品数量不限.

#include<bits/stdc++.h>
use namespace std;
const int maxn=100005;
int w[maxn],v[maxn]
int dp[maxn];
int main()
{
    int n,maxv;   cin>>n>>maxv;
    int index=0;
    for(int i=0;i<n;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        while(a--) {
            w[index]=b;
            v[index++]=c;
        }
    }
    dp[0]=1;
    for(int i=0;i<index;i++)
    {
        for(int j=n;j>=w[i];j--)
        {
            dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
        }
    }
   cout<<dp[maxv]<<endl;
    return 0;
}

  

另外九度教程上给出了一种方法,将原数量为k的物品拆分成若干组,每一组可看成一件新的物品,其价值和重量为改组中所有物品的价值重量的总和,每组物品包含的原物品个数分别为:1、2、4...k-2^c+1,其中c为使k-2^c+1大于0的最大整数。这样就将物品数量大大降低,同时通过对这些若干个原物品组合得到的新物品的不同组合,可以得到0到k之间的任意件物品的价值重量和,所以对所有这些新物品做0-1背包,即可得到多重背包的解。转化之后的时间复杂度为O(V*∑ni=1log2(ki))。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!