zoukankan      html  css  js  c++  java
  • 01背包问题 之 动态规划(通俗解释)

    01背包问题 (问题描述)

    给定 n 件物品,物品的重量为 w[i],物品的价值为 c[i]。现挑选物品放入背包中,假定背包能承受的最大重量为 V,问应该如何选择装入背包中的物品,使得装入背包中物品的总价值最大?

    一个有趣的例子

    假设你是一个小偷,背着一个可装下4磅东西的背包,你可以偷窃的物品如下:

    为了让偷窃的商品价值最高,你该选择哪些商品?

    重量价值表
    物品名 重(磅) 价值(美元)
    吉他 1 1500
    音响 4 3000
    笔记本 3 2000

    直接上手尝试组合在这种数量比较少的情况还可行,直接可得出偷 吉他和笔记本,总重4磅,价值3500美元。然而要是换一个问题,比如最近 B 站首届bilibili 1024 安全挑战赛的一道题如下:

    3.期末考试结束了,老师决定带学生们去卷饼店吃烤鸭饼。老师看到大饼和鸭子,搞了一个活动:每人可以拿走一张饼,谁卷到的食物美味程度总和最高,谁就能获得称号:卷王之王!Vita很想得到“卷王之王”称号,他的大饼可以装下大小总和不超过500的食物,现在有7块鸭肉和6根黄瓜,每份食物都有它的大小和美味程度。 每块鸭肉的大小:85、86、73、66、114、51、99 每块鸭肉的美味程度:71、103、44、87、112、78、36 每根黄瓜的大小:35、44、27、41、65、38 每块黄瓜的美味程度:41、46、13、74、71、27 老师要求大饼里至少有一块鸭肉和一根黄瓜。请问,Vita卷到的食物美味程度总和最大是多少?(本题由UP主@小学生Vita君提供)

    A. 593   B.612    C.496   D. 584

    直接尝试组合恐怕不是一件容易的事,我们需要一个更便捷的策略来辅助我们解决这样的问题。

    这里要尝试解释的,就是耳熟能详的动态规划,百度百科里这样解释动态规划的基本思想:

    动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。

    至于为啥叫 “动态”,百度百科里这样解释其概念:

    在现实生活中,有一类活动的过程,由于它的特殊性,可将过程分成若干个互相联系的阶段,在它的每一阶段都需要作出决策,从而使整个过程达到最好的活动效果。因此各个阶段决策的选取不能任意确定,它依赖于当前面临的状态,又影响以后的发展。当各个阶段决策确定后,就组成一个决策序列,因而也就确定了整个过程的一条活动路线.这种把一个问题看作是一个前后关联具有链状结构的多阶段过程就称为多阶段决策过程,这种问题称为多阶段决策问题。在多阶段决策问题中,各个阶段采取的决策,一般来说是与时间有关的,决策依赖于当前状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生出来的,故有“动态”的含义,称这种解决多阶段决策最优化的过程为动态规划方法

    这些定义虽然很准确,但是确实有一定程度的抽象,如果在理解的时候能够把这个理解具体化,那就好理解一些。所以接下来还是回到上面那个小偷的例子,一步一步构建出动态规划的核心:状态表格。

    关于构建状态表格

    网格的各行表示商品,各列代表不同容量(1~4磅)的背包

    1. 在填第一行时,我们可选的商品只有吉他;在填第二行时,我们可选的商品则可以有吉他和音响;同理,在填第三行时,可选吉他,音响,笔记本电脑

    2. 最后一行的最后一列,对应于这里的第三行第四列所填写的数字,表示在背包容器为 4 的情况下,从吉他,音响,笔记本选出的最佳组合的价值。 

    3. 用动态规划解决 01背包问题 的巧妙之处类似于递归的优雅简洁,你要做的就是将一个看似复杂的问题,分解为自相似的子问题

    n! = n *(n-1)!       //求一个数 n 的阶乘,可以将其分解为: 使用 n 乘以 n-1 的阶乘

    这里背包问题的分解方式是:求能装4磅背包装什么价值最高,分解为假设我先拿下一个笔记本电脑(3磅),剩下的容量(1磅) 能够装什么价值最高(子问题)

    这里构建的表格,每一个格子就是一个问题(求能装某个磅数重量的背包装什么价值最高),而该问题的子问题答案,总能在前面已经构建的表格中找到答案

    越往前问题就越小,最前面的一行自然是最最简单的,所以,这里构建表格的方式是:

    4. 我们将一行一行从上往下填

    开始构建表格

    第一行

    这一行,我们可选的商品只有吉他

    第一个单元格表示背包的的容量为1磅。吉他的重量也是1磅,这意味着它能装入背包!因此这个单元格包含吉他,价值为1500美元。

     与这个单元格一样,每个单元格都将包含当前可装入背包的所有商品。

    来看下一个单元格。这个单元格表示背包容量为2磅,完全能够装下吉他!

     这行的其他单元格也一样。别忘了,这是第一行,只有吉他可供你选择,换而言之,你假装现在还没发偷窃其他两件商品

     此时你很可能心存疑惑:原来的问题说的额是4磅的背包,我们为何要考虑容量为1磅、2磅等得背包呢?前面说过,动态规划从小问题着手,逐步解决大问题。这里解决的子问题将帮助你解决大问题。

    别忘了,你要做的是让背包中商品的价值最大。这行表示的是当前的最大价值。它指出,如果你有一个容量4磅的背包,可在其中装入的商品的最大价值为1500美元。

    你知道这不是最终解。随着算法往下执行,你将逐步修改最大价值。

    第二行

    你现在处于第二行,可以偷窃的商品有吉他和音响。

    我们先来看第一个单元格,它表示容量为1磅的背包。在此之前,可装入1磅背包的商品最大价值为1500美元。

    该不该偷音响呢?

    背包的容量为1磅,显然不能装下音响。由于容量为1磅的背包装不下音响,因此最大价值依然是1500美元。

     接下来的两个单元格的情况与此相同。在这些单元格中,背包的容量分别为2磅和3磅,而以前的最大价值为1500美元。由于这些背包装不下音响,因此最大的价值保持不变。

     背包容量为4磅呢?终于能够装下音响了!原来最大价值为1500美元,但如果在背包中装入音响而不是吉他,价值将为3000美元!因此还是偷音响吧。

     你更新了最大价值。如果背包的容量为4磅,就能装入价值至少3000美元的商品。在这个网格中,你逐步地更新最大价值。

    第三行:

    下面以同样的方式处理笔记本电脑。笔记本电脑重3磅,没法将其装入1磅或者2磅的背包,因此前两个单元格的最大价值仍然是1500美元。

     对于容量为3磅的背包,原来的最大价值为1500美元,但现在你可以选择偷窃价值2000美元的笔记本电脑而不是吉他,这样新的最大价值将为2000美元。

     对于容量为4磅的背包,情况很有趣。这是非常重要的部分。当前的最大价值为3000美元,你可不偷音响,而偷笔记本电脑,但它只值2000美元。

     价值没有原来高,但是等一等,笔记本电脑的重量只有3磅,背包还有1磅的重量没用!

     在1磅的容量中,可装入的商品的最大价值是多少呢?你之前计算过。

     根据之前计算的最大价值可知,在1磅的容量中可装入吉他,价值1500美元。因此,你需要做如下的比较:

    你可能始终心存疑惑:为何计算小背包可装入的商品的最大价值呢?但愿你现在明白了其中的原因!余下了空间时,你可根据这些子问题的答案来确定余下的空间可装入哪些商品。笔记本电脑和吉他的总价值为3500美元,因此偷它们是更好的选择。
     
    最终的网格类似于下面这样。

     答案如下:将吉他和笔记本电脑装入背包时价值更高,为3500美元。

    你可能认为,计算最后一个单元格的价值时,我使用了不同的公式。那是因为填充之前的单元格时,我故意避开了一些复杂的因素。其实,计算每个单元格的价值时,使用的公式都相同。这个公式如下。

    同样的容量,在原来考虑的商品之上,多考虑一个商品,只有两种可能:“要么考虑先加入当前商品,要么不考虑直接还是用原先的组合”。哪种更好,选择哪个。

    这个,就是动态规划求解背包问题的核心秘诀。

    第四行:

    知道这个秘密后,我们再来添加一个商品,巩固一下这个计算方式:

    现在假设还有第四件商品可偷——一个iPhone

     此时需要重新执行前面所做的计算吗?不需要。别忘了,动态规划逐步计算最大价值。到目前为止,计算出的最大价值如下:

    这意味着背包容量为4磅时,你最多可偷价值3500美元的商品。但这是以前的情况,下面再添加表示iPhone的行。

     我们还是从第一个单元格开始。

    “要么考虑先加入当前商品,要么不考虑直接还是用原先的组合”

    iPhone可装入容量为1磅的背包,放下之后填满没有剩余空间。之前的最大价值为1500美元,但iPhone价值2000美元,因此该偷iPhone而不是吉他。

    在下一个单元格中,

    “要么考虑先加入当前商品,要么不考虑直接还是用原先的组合”

    2磅容器,考虑 1磅的 iPhone,剩下 1 磅空间,1500美元的吉他,共可装入iPhone和吉他,比 刚刚不用 iPhone 好。

     对于第三个单元格,

    “要么考虑先加入当前商品,要么不考虑直接还是用原先的组合”

    3磅容器,考虑 1磅的 iPhone,剩下 2 磅空间,根据前面的评估,也是剩下1500美元的吉他,也是装入iPhone和吉他 比 刚刚不用 iPhone 好。

    对于最后一个单元格,情况比较有趣。

    同样,“要么考虑先加入当前商品,要么不考虑直接还是用原先的组合”

    当前的最大价值为3500美元,但你可以偷iPhone,这将余下3磅的容量。

     3磅容量的最大价值为2000美元!再加上iPhone价值2000美元,总价值为4000美元。新的最大价值诞生了!

    最终的网格如下。

    代码实现参考:

    #include <vector>
    #include <iostream>
    
    //解决方案
    class solution
    {
    public:
        solution(int value):totalValue(value) {}
        int totalValue;             //选择的物品的总价值
        std::vector<size_t> items;  //选择的物品的项
        int containerValue;         //容器容量
    };
    
    //构建网格
    solution buildNet(const std::vector<size_t>& w, const std::vector<int>& v, size_t total)
    {
        size_t row = w.size();      //可选择的物体数量
        size_t column = total;      //总容量
    
        std::vector<std::vector<solution>> net;
        net = std::vector<std::vector<solution>>(row+1, std::vector<solution>(column+1, 0));  //初始化多第一行和第一列,便于通用公式
    
        for (size_t r = 1; r <= row; ++r)
        {
            for (size_t c = 1; c <= column; ++c)
            {
                size_t weightCurrent = w[r - 1]; //当前物品重
                int valueCurrent = v[r - 1];  //当前物品价值
                if (weightCurrent <= c)       //如果单独放得下
                {
                    int valueIncludeCurrent = valueCurrent + net[r - 1][c - weightCurrent].totalValue;
                    if (valueIncludeCurrent > net[r - 1][c].totalValue) //加入当前物品价值更高,则更新方案
                    {
                        net[r][c] = valueIncludeCurrent;
    
                        net[r][c].items = net[r - 1][c - weightCurrent].items; //得到之前的序列
                        net[r][c].items.push_back(r);                          //添加自己到序列后
                    }
                    else
                        net[r][c] = net[r - 1][c];
                }
                else
                    net[r][c] = net[r - 1][c];
            }
        }
    
        net[row][column].containerValue = total;
        return net[row][column];
    }
    
    //打印选择的最佳方案
    void printVector(const std::vector<size_t>& w, const std::vector<int>& v,const solution & s)
    {
        std::cout << "Input: ";
        for (size_t i = 0; i < w.size(); ++i)
        {
            std::cout << w[i] << " (" << v[i] << ");" ;
        }
        std::cout << "Container: " << s.containerValue << std::endl;
    
        const std::vector<size_t>& items = s.items;
        int totalV = s.totalValue;
    
        size_t totalW = 0;
        size_t totalV2 = 0;
        for (auto r : items)
        {
            size_t w0 = w[r-1];
            int v0 = v[r-1];
            std::cout << w0 << " (" << v0 << ");" << std::endl;
    
            totalW += w0;
            totalV2 += v0;
        }
        std::cout << "Total: " << totalW << " (" << totalV << " -> check:" << totalV2 << ")";
        std::cout << std::endl << std::endl;
    }
    
    int main()
    {
         std::vector<size_t> w = {1,4,3,1};
         std::vector<int> v = { 1500,3000,2000,2000};
         solution maxValue = buildNet(w, v, 4);
         printVector(w, v, maxValue);
    
         std::vector<size_t> w2 = { 85, 86, 73, 66, 114, 51, 99 };
         std::vector<int> v2 = { 71,103,44, 87, 112, 78, 36 };
         solution duck = buildNet(w2, v2, 500);
         printVector(w2, v2, duck);
    
         std::vector<size_t> w3 = { 35, 44,  27,  41, 65, 38 };
         std::vector<int> v3 = { 41, 46, 13, 74, 71, 27 };
         solution cucumber = buildNet(w3, v3, 500);
         printVector(w3, v3, cucumber);
    
         std::vector<size_t> w4 = w2;  w4.insert(w4.end(), w3.begin(), w3.end());
         std::vector<int> v4 = v2; v4.insert(v4.end(), v3.begin(), v3.end());
         solution duckCucumber = buildNet(w4, v4, 500);
         printVector(w4, v4, duckCucumber);
    
        return 0;
    }
    01背包算法 动态规划实现

     


    参考1:(0-1背包问题 - 简书 by 我没有三颗心脏):https://www.jianshu.com/p/a66d5ce49df5

    参考2:(动态规划之01背包问题 - 简书 by kkbill):https://www.cnblogs.com/kkbill/p/12081172.html

    本文原文地址:https://www.cnblogs.com/BensonLaur/p/15464045.html


    版权声明
    本博客所有的原创文章,作者皆保留版权。转载必须包含本声明,保持本文完整,并以超链接形式注明作者 BensonLaur  和本文原始地址:
  • 相关阅读:
    RPI学习--环境搭建_更新firmware
    RPI学习--环境搭建_刷卡+wiringPi库安装
    [转]VS2005 Debug时提示"没有找到MSVCR80D.dll"的解决办法
    [转]结构体字节对齐
    [转]C++运算优先级列表
    putty基本操作
    Go 修改字符串中的字符(中文乱码)
    Go part 5 结构体,方法与接收器
    pickle 和 base64 模块的使用
    原来还有 卡夫卡 这个人存在
  • 原文地址:https://www.cnblogs.com/BensonLaur/p/15464045.html
Copyright © 2011-2022 走看看