• 背包问题-面试中的动态规划


    背包问题-面试中的动态规划

    序言

    背包问题是动态规划(Dynamic Programming)中一类经典的问题,弄懂背包问题对于理解DP有很大的帮助。在程序员的面试当中也会有许多背包问题的缩影。本文收集了一些Online Judge平台中的例子来介绍各类背包问题的情形。面试当中遇到相关的DP问题,可以举一反三。

    网上也有很多讲背包问题的文章,之所以写这篇文章是想作一个的总结,
    以便自己以后查看。也希望能够给读者带来一些帮助。
    如果想要系统了解背包问题的理论知识,
    《背包问题九讲》是很好的学习资料,但是其中给出的伪代码到可运行的源代码差距还比较大,本文给出了所有的案例的源代码实现。

    01背包

    从最简单的01背包开始讲起。
    先看一个题目吧

    Q1:
    在N个物品中挑选若干物品装入背包,最多能装多满?
    假设背包的大小为V,第i个物品的大小为C[i]

    注意事项:
    你不可以将物品进行切割。
    样例:

    • 如果有4个物品[2, 3, 5, 7]
    • 如果背包的大小为11,可以选择[2, 3, 5]装入背包,最多可以装满10的空间。
    • 如果背包的大小为12,可以选择[2, 3, 7]装入背包,最多可以装满12的空间。
    • 函数需要返回最多能装满的空间大小。

    这道题目来自于lintcode-backpack

    //AC code
    class Solution {
    private:
        int V;
    
    public:
        Solution():V(0) {}
        void ZeroOnePack(int F[], int C, int W) {
            for (int v = V; v >= C; v--) {
                F[v] = max(F[v], F[v-C] + W);
            }
        }
        /**
         * @param V: An integer V denotes the size of a backpack
         * @param C: Given n items with size C[i]
         * @return: The maximum size
         */
        int backPack(int V, vector<int> C) {
            int N = C.size();
            this->V = V;
            int f[V+1];
            // 初始化
            for (int i = 0; i <= V; i++) {
                f[i] = 0;
            }
            for (int i = 1; i <= N; i++) {
                ZeroOnePack(f, C[i-1], C[i-1]);
            }
            return f[V];
        }
    };
    

    另外一个类似的题目:

    Q2:
    有N件物品和一个容量为V的背包。放入第i件物品耗费的费用是Ci,得到的价值是Wi。求解将哪些物品装入背包可使价值总和最大?

    这道题来自于lintcode-backpack-ii
    只需要将Q1的AC代码中ZeroOnePack的第三个参数改为W[i-1],即可得到Q2的AC代码。

    Q1是Q2的一种特殊形式,即物品的价值就是物品本身。因为Q1的问题是如何才能使得背包装的最满,意思就是说物品的体积就是物品的价值。Q2只不过是将价值换成了一种其他的说法。
    Q1,Q2是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放
    用子问题定义状态:即F[i,v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。
    其状态转移方程便是:F[i,v] = max{F[i−1, v], F[i−1, v−Ci] + Wi}
    这个方程是什么意思呢?用通俗易懂的语言来描述:就是假设我们已经知道了有i件物品放入v背包中的方案,其获得的最大的价值用F[i,v]来表示。那么在这个方案中,我们来研究一下第i件物品的策略(放或者不放)。如果不放第i件物品,那么问题就转化为“前i−1件物品放入容量为v的背包中”,价值为F[i−1, v];如果放第i件物品,那么问题就转化为“前i−1件物品放入剩下的容量为v−Ci的背包中”,此时能获得的最大价值就是F[i−1, v−Ci]再加上通过放入第i件物品获得的价值Wi。

    //伪代码
    def ZeroOnePack(F, C, W)
      for(int v = V; v >= C; v--)
        F[v] = max(F[v], F[v-C]+W)
    
    def main
      int F[V+1]
      //初始化
      for(int i = 0; i <= V; i++)
        F[0] = 0;
      for(int i = 1; i <= N; i++)
          ZeroOnePack(F, C[i], W[i]))
    

    这儿使用了一个优化空间的手段,可以看到我们的状态转移方程中使用的是一个二维变量F[i,v],而在伪代码中这个二维变量变成了F[V]。因为我们最后要求的结果是N个物品放入到容量为V的包中所能获得的最大价值,所以我们只关心最后的F[N,V]这个值是多少?而F[N,V]依赖于F[N-1,V]和F[N-1,V-C[N]]。
    可以看到在main函数中i是从1一直自增到N的,因此我们用F[V]来表示每次循环后的结果,即F[i,V]。那么当i变为i+1的时候,计算F[i+1,V]时用到的F[i,V-C[i+1]],即F[V-C[i+1]]已经被覆盖(V-C[i+1] < V)。这儿只要从v=V自减便能解决这个问题。

    还有一个关于初始化的问题:关于背包问题,通常有两种不太相同的问法。有的题目要求“恰装满背包”时的最优解,有的题目并没有要求必须把背包装满。针对于这两种不同的问法,实现时表现在于初始化的时候有所不同。如果是第一种问法,要求恰好装满背包,那么在初始化时除了F[0]为0,其它F[1..V]均设为−∞,这样就可以保证最终得到的F[V]是一种恰好装满背包的最优解。如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将F[0..V]全部设为0。

    再看一个要求正好装满的题目吧

    Q3:给出一个都是正整数的数组 nums,其中没有重复的数。从中找出所有的和为 target 的组合个数。
    一个数可以在组合中出现多次。数的顺序不同则会被认为是不同的组合。
    样例:
    给出 nums = [1, 2, 4], target = 4
    可能的所有组合有:
    [1, 1, 1, 1]
    [1, 1, 2]
    [1, 2, 1]
    [2, 1, 1]
    [2, 2]
    [4]
    返回 6

    这道题目来自于lintcode-backpack-vi

    //AC code
    class Solution {
    public:
        int backPackVI(vector<int>& nums, int target) {
            // Write your code here
            int dp[target+1];
            for(int i = 0; i < target + 1; i++) {
                dp[i] = 0;
            }
            dp[0] = 1;
            for (int i = 1; i <= target; ++i) {
                for (auto a : nums)
                if (i >= a) {
                    dp[i] += dp[i - a];
                }
            }
            return dp[target];
        }
    };
    

    完全背包问题

    Q3中的场景明显与Q1和Q2不相同,Q1和Q2当中,物品的最大一个特点是:每件物品只能使用一次。
    Q3描述的是一个新的问题:完全背包问题。在完全背包问题当中,每件物品的数量是没有限制的。

    这儿再次描述一下完全背包问题:有N种物品和一个容量为V的背包,每种物品都有无限件可用。放入第i种物品
    的费用是Ci,价值是Wi。求解:将哪些物品装入背包,可使这些物品的耗费的费用总和不超过背包容量,且价值总和最大[1]
    注意到完全背包问题是在01背包问题的基础上将每件物品的数量限制去掉了。
    那么,很自然地,我们可以将完全背包问题转换成为01背包问题求解。
    先给出状态转移方程: F[i,v]=max(F[i−1,v],F[i,v−Ci]+Wi)
    这个状态转移方程的意思是:将前i件物品放入容量为v的背包中获取到的最大价值等价于当第i件
    物品一件都不放入时,前i-1件物品放入容量为v的背包中获取到的最大价值;或者当放入第i件物品
    时,前i件物品放入容量为v-Ci的背包中获取到的最大价值,注意这儿并不是前i-1件而仍然是前i件。

    //伪代码
    def CompletePack(F, C, W)
       for v <-- C to V
          F[v] = max{F[v], F[v-C]+W}
    

    上述伪代码和01背包伪代码唯一的区别只有v的循环次序不同。

    再回过头看一眼Q3:target即为背包容量,nums数组给出了每件物品的费用,物品本身的费用即为其价值。另外一类换零钱的问题也比较类似。
    可以将target视为一张整钱,nums里面提供的是零钱的面额,此题即要求出换钱的方法和。
    AC code中代码的意思是:dp[i]表示将nums中的物品放入容量为i的背包中的总的方法数目。因为问题是求总的换法数目,所以需要将动态转移方程中的max改为sum。dp[i]+=dp[i-a]的意思是有两种可能,a对应的物品不放入背包或者放入背包。

    将Q3稍微改变一下:

    Q4: 某个国家发行了3种不同的硬币,面值分别为1元,2元和4元。现在某人有一张面值为4元纸币,他需要将纸币换成硬币,问共有多少种换法?和Q3不一样的地方在于,序列[1,2,1]和[1,1,2],[2,1,1]等属于一种换法。

    其实,这也是一道完全背包问题。假设F[i,j]表示只用前i种硬币将j兑换的种数,那么动态转移方程就是:
    F[i,j] = F[i - 1, j] + F[i, j - C]
    再做一下空间上的优化,代码如下:

    class Solution {
    public:
        static int V;
        template <class T>
        void printArray(T arr[], int len) {
            for(int i = 0; i < len; i++)
                cout<<arr[i]<<" ";
            cout<<endl;
        }
    
        void CompletePack(int F[], int C, int W) {
            for(int v = C; v <= V; v++) {
                F[v] = max(F[v], F[v - C] + W);
            }
            //printArray(F, V+1);
        }
    
        /**
         * @param nums an integer array and all positive numbers, no duplicates
         * @param target an integer
         * @return an integer
         */
        int backPackVI(vector<int>& nums, int target) {
            //this is a Complete pack
            int size = nums.size();
            V  = target;
            int f[V+1];
    
            for(int v = 0; v <= V; v++) {
                f[v] = 0;
            }
            f[0] = 1;
    
            for(int i = 1; i <= size; i++) {
                //printf("index %d: call CompletePack -> C[%d] W[%d]
    ", i, nums.at(i-1), nums.at(i-1));
                CompletePack(f, nums.at(i-1), nums.at(i-1));
            }
            return f[V];
        }
    };
    
    int Solution::V = 0;
    

    updating..
    多重背包

    //Pseudo Code
    def ZeorOnePack(F, C, W)
         for( v = V; v >= C; v--)
             F(v) = max(F(v), F(v-C) + W)
    
    def CompletePack(F, C, W)
         for(v = C; v <= V; v++)
             F(v) = max(F(v), F(v-C) + W)
    
    def MultiplePack(F, C, W, M)
         if(C * M >= V)
             CompletePack(F, C, V)
             return
         k = 1
         while( k < M )
             ZeroOnePack(F, kC, kW)
             M -= k;
             k *= 2;
         ZeroOnePack(F, C * M, W * M)
    
    def Main
         for( v = 0; v < V; v++)
             F(v) = 0
         MultiplePack(F, C, W, M)
    
    本文很多内容来自互联网,如有侵权,请联系作者

    [参考文献]

    1. 背包问题九讲-崔添翼
  • 相关阅读:
    .NET Framework 3.0 和 Windows SDK
    用 C# 开发 SQL Server 2005 的自定义聚合函数
    IronPython 源码剖析系列(2):IronPython 引擎的运作流程
    IronPython 个人网站样例宝藏挖掘
    SetRenderMethodDelegate 方法
    使用 Castle ActiveRecord 开发发现的一些问题
    IronPython for ASP.NET CTP WhitePaper 摘要翻译
    关于 IE 模态对话框的两个问题
    逐步改用 IronPython 开发你的 ASP.NET 应用程序
    使用 Flash 和 C# WinForm 配合打造界面漂亮的应用程序(摘要)
  • 原文地址:https://www.cnblogs.com/longf0720/p/7367721.html
Copyright © 2020-2023  润新知