01背包 初学篇

关于更多背包的内容可以在我的“背包”类别中查看

01背包

一、经典题目

有n件物品和一个容量为m的背包,放入第i件物品所需要的空间为wi,第i件物品的价值为vi,问背包可以放入物品的最大价值为多少?

二、基本思路

初学典型疑问:贪心,找出单位价值最大的,不就可以了吗,那么举出一个例子
当有一个背包的容量为10,共有3个物品,体积分别是3、3、5,价值分别是6、6、9,那么你的方法取到的是前两个物品,总价值是12,但明显最大值是后两个物品组成的15。


如果直接求解n件物品放入背包的最大价值,时间复杂度和空间复杂度都会很高,所以我们使用动态规划的算法来求解。


什么是动态规划呢?

(这里可以先隔过去往下看,通过01背包问题初步了解一下动态规划,再看它的原理则更好理解。)

动态规划是把大问题拆分成小问题,通过寻找大问题与小问题的递推关系,解决一个个小问题,最终达到解决原问题的效果。动态规划则通过填写表把所有已经解决的子问题答案纪录下来,在新问题里需要用到的子问题可以直接提取,避免了重复计算,从而节约了时间,所以在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到。


借鉴递归的思想,我想知道放入n件物品最大价值是多少,我只需要知道放入n-1件物品最大价值是多少就行,因为这是我只需要决定是不放第n件物品还是要第n件物品,同理,我想知道放入n—1件物品最大价值是多少,我只需要知道放入n-2件物品最大价值是多少,这样最后只有一件物品时,我只需要知道现在能不能放进去就行了,能放进去就是一件物品的价值,放不进去就是0,然后我们递归回来,就可以知道我们想求得答案了。这边是01背包通过递归实现的思路。
当然我们求解01背包问题并不需要使用递归(不是不可以),我们把这一思想通过状态转移方程即可实现.
我们求出背包容量从0——m时放入0——n件物品的最大值,列出这个二维数组,那么(i=m,j=n)是不就是我们要求的结果了吗。

状态转移方程:F[i,j]=max{F[i-1,j],F[i-1,j-wi]+vi}

F[i,j]是i件物品放入背包的最大价值,F[i-1][j]代表的就是不将第i件物品放入背包,而f[i-1][j-w[i]]+v[i]则是代表将第i件放入背包之后的总价值,比较两者的价值,得出最大的价值存入现在的背包之中。

三、例题

或许到这里大家对这个方程还不是那么熟悉,我们便通过一个实例来走一遍:

物品件数n=4,背包容量m=8
物品编号1234
w(体积)
v(价值)

初始化:
这里写图片描述
总表:
这里写图片描述

现在挑几个典型的说一下这个表是怎么更新的:
点(i=2,j=3):这时有两个物品可以放入,背包容量为3,此时我们面临一个抉择,放还是不放第二件物品,我们看一下,上一状态,也就是只有一件物品或者说我们不放这件物品时的最大价值(i=1,j=3)为3,再看一下如果我们放这件物品的最大价值,(此时空间明显不足以同时放入这两件物品,我们如果放这件物品总得给它留出足够的空间吧,所以我们计算一下当给他留出足够空间时,空间还剩多少,此时背包的最大价值是多少)(i=1,j=3-3=0)为0(也就是说此时我如果想放入第二件物品的话,就得把第一件物品拿出来,拿出来后背包价值是0),再把第二件物品放进去,此时背包价值为4,我们比较一下这两个状态,如果不放第二件物品背包价值为3,放第二件物品背包价值为4,我们当然选择翻入第二件物品。
如果这个点还是不太明白我们再试一个点,算法是一样的
点(i=3,j=7):这时有三个物品可以放入,背包容量为7,我们面临一个抉择,放还是不放第三个物品,

这个地方我一开始有点迷,点(i=3,j=6)的时候我已经把第三个物品放进去了,那还这个点我还
看放不放他干啥,这时候不还得判断是不是得扔哪个物品吗?是不是有点傻?这时候我们的上一状
态是背包容量为j时,有两个物品可放的最大价值,可以说i相等时,每个点都是独立的,互不相
关,所以此时我们比较的是(i=2,j=7)这个点,要看的是,当有二个物品可以放入,背包容量为7
时再放入第三个物品能不能使背包的价值更大。

我们看一下,上一状态,也就是只有一件物品或者说我们不放这件物品时的最大价值(i=2,j=7)为7,再看一下如果我们放这件物品的最大价值,(此时空间明显不足以同时放入这两件物品,我们如果放这件物品总得给它留出足够的空间吧,所以我们计算一下当给他留出足够空间时,空间还剩多少,此时背包的最大价值是多少)(i=2,j=7-4=3)为4(也就是说要想放入第三个物品得给给他让出v[3]=4的容量,此时背包容量还剩3,我们可以知道,背包容量为3时背包最大价值为(i=2,j=3)=4),再把第三件物品放进去,此时背包价值为4+5=9,我们比较一下这两个状态,如果不放第三件物品背包价值为7,放第二件物品背包价值为9,我们当然选择翻入第三件物品。

CODE:

#include <iostream>

using namespace std;

int v[105], w[105];
int dp[105][1005];

int main()
{
    int n, m, res=-1;
    cin >> n >> m;
    for(int i=1; i<=m; i++)
        cin >> w[i] >> v[i];

    for(int i=1; i<=m; i++) //物品 
        for(int j=n; j>=0; j--) //容量 
        {
            if(j >= w[i])
                dp[i][j] = max(dp[i-1][j-w[i]]+v[i], dp[i-1][j]);
            else      //只是为了好理解
                dp[i][j] = dp[i-1][j];           
        }
    cout << dp[m][n] << endl;
    return 0;
}

由于上面已经长篇大论了,此处注释并不给太多了.


四、优化空间复杂度

以上方法的时间和空间复杂度均为 O(mn ) ,其中时间复杂度应该已经不能再优化
了,但空间复杂度却可以优化到 O(m ) 。
我们可以通过二维数组的方法来理解01背包,但是我非常推荐用一维数组的方法,因为后面的完全背包的CODE和一维数组的CODE非常相似
由上面的图可以看出来,每一次dp(i)(j)改变的值只与dp(i-1)(x) {x:1…j}有关,dp(i-1)(x)是前一次i循环保存下来的值;
因此,可以将V缩减成一维数组,从而达到优化空间的目的.

状态转移方程转换为 F(j)= max{F(j), F(j-v(i))+w(i)};

注意:一维数组的j扫描顺序是从后往前扫也就是从m-》0,因为如果从前往后扫的话,上一次的状态会被现在的状态覆盖掉。比如上题中,如果从前往后扫的话,当有三件物品可以放入时,可以看到(3,4)点扫过后值变成了5,当更新点(3,8)时,我们比较的是(3,4)和(3,8)

(此时(3,8)值还为上一状态的值,也就是还为二维数组中(2,8)的值)

经过比较后,我们得到的结果变成了5+5=10,明显和真实的值4+5=9不符。
而我们从后往前扫就不会出现这样的错误,因为我们更新的是后面的值,而我们比较的是前面的值,所以不影响

CODE:

//求解将哪些物品装入背包可使这些物品的重量总和不超过背包承重量t,且价值总和最大。
#include <stdio.h>
#include <string.h>

int f[1010],w[1010],v[1010];//f记录不同承重量背包的总价值,w记录不同物品的重量,v记录不同物品的价值

int max(int x,int y){//返回x,y的最大值
    if(x>y) return x;
    return y;
}

int main(){
    int t,m,i,j;
    memset(f,0,sizeof(f));  //总价值初始化为0
    scanf("%d %d",&t,&m);  //输入背包承重量t、物品的数目m
    for(i=1;i<=m;i++)
        scanf("%d %d",&w[i],&v[i]);  //输入m组物品的重量w[i]和价值v[i]
    for(i=1;i<=m;i++){  //尝试放置每一个物品
        for(j=t;j>=w[i];j--){//倒叙是为了保证每个物品都使用一次
            f[j]=max(f[j-w[i]]+v[i],f[j]);
            //在放入第i个物品前后,检验不同j承重量背包的总价值,如果放入第i个物品后比放入前的价值提高了,则修改j承重量背包的价值,否则不变
        }
    }
    printf("%d",f[t]);  //输出承重量为t的背包的总价值
    printf("\n");
    getch();
    return 0;
}

记录路径

我们如果想知道背包里都放了哪些物品该怎么做呢?
我们只需要用一个二维数组来标记一下每次更新放入的物品就行了
然后我们从后往前遍历,输出每次加入的物品
为什么是从后往前呢?
因为从后往前每一步都是在最优解的情况下进行的,而从前往后,每一步都不能确定是不是最优解。比如:
第一步我们判断是不是要把第一个物体放入背包中,然而此时我们还不能确定有n个物体时,第一个物体放入是不是最优解,然而我们在第n步是可以确定第n个物品是不是应该放入。

CODE:

#include <stdio.h>
#include <string.h>
#include <iostream>
#include <algorithm>
#include <cmath>
#include <stack>
#include <queue>
#include <functional>
#include <time.h>
using namespace std;
#define inf 0x3f3f3f3f
#define PI acos(-1)

int main()
{
    int m,n;
    int w[110];
    int v[110];
    int dp[110];
    int path[110][110];//标记数组
    memset(path,0,sizeof(path));
    scanf("%d%d",&m,&n);
    for(int i=1; i<=m; i++)
        scanf("%d%d",&w[i],&v[i]);
    for (int i = 1; i <= m; i++)
        for (int j = n; j >= w[i]; j--)
            if (dp[j] < dp[j - w[i]] + v[i])
            {
                dp[j] = dp[j - w[i]] + v[i];
                path[i][j] = 1;
            }
    int i = m, j = n;
    while (i > 0 && j > 0)
    {
        if (path[i][j] == 1)
        {
            cout << i << ' ';
            j -= w[i];
        }
        i--;
    }
    cout << endl;
    cout << "总的价值为: " << dp[n] << endl;
    return 0;
}

五、构造ZeroOnePack函数

提前剧透一下,在我们以后要学习的多重背包中我们是会频繁的使用01背包的,所以我们把它写成函数形式,以便使用。

CODE:

void ZeroOnePack(int c, int w)  
{  
    for (int i = V; i >= c; i--)  
    {  
        dp[i] = max(dp[i], dp[i - c] + w);  
    }  
} 

六、初始化中的细节

我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰
好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的
实现方法是在初始化的时候有所不同。
如果是第一种问法,要求恰好装满背包,那么在初始化时除了 F [0] 为 0 ,其它F [1..V ] 均设
为 −∞ ,这样就可以保证最终得到的 F [V ] 是一种恰好装满背包的最优解。如果并没有要
求必须把背包装满,而是只希望价格尽量大,初始化时应该将 F [0..V ]全部设为 0 。
这是为什么呢?可以这样理解:初始化的 F 数组事实上就是在没有任何物品可以放入背包时
的合法状态。如果要求背包恰好装满,那么此时只有容量为 0 的背包可以在什么也不装且
价值为 0 的情况下被“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,应该
被赋值为 -∞ 了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都
不装”,这个解的价值为 0 ,所以初始时状态的值也就全部为 0了。
这个小技巧完全可以推广到其它类型的背包问题,后面不再对进行状态转移之前的初始化
进行讲解。
由于背包九讲中讲的比较明白,所以就直接摘过来。
------------------------------摘自《背包九讲》

七、一个常数优化(同摘自《背包九讲》暂时未研究)

上面伪代码中的
for i ← 1 to N
for v ← V to C i
中第二重循环的下限可以改进。它可以被优化为
for i ← 1 to N
for v ← V to max(V − NiW , C i )
这个优化之所以成立的原因请读者自己思考。(提示:使用二维的转移方程思考较易。)

八、推荐博客:

  1. http://www.cnblogs.com/aiguona/p/7274222.html 自家师哥的博客,贴了一维和二维数组的模板,图示很好
  2. http://www.cnblogs.com/SDJL/archive/2008/08/22/1274312.html  通过金矿开采介绍了01背包,虽然比较长但是易于理解

到了这里01背包大体也就算说完了,相关的题目会在别的文章中写出,只需要在“背包”类别中查找即可(暂时未写),至于其他的优化或者思想,注意事项,在我做题中遇到会来更新,也请各位dalao补充和指错。

  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值