zoukankan      html  css  js  c++  java
  • 从背包问题说起——初学者角度看背包问题

    从背包问题说起

    本文聚焦DP、记忆化搜索、滚动数组在0-1背包问题中的运用,并简要探讨多重背包问题中拆分思想的实践。

    背包问题分为几大类,0-1背包问题、完全背包问题、多重背包问题、混合背包问题。0-1背包问题中每个对象只能选取一次,其他问题都是在0-1背包基础上进行推演的。

    0-1背包

    以0-1背包问题的表述为例:有n个物品和容量为(W)的背包,每个物品分别具有(wi)和价值(vi)两个属性,求W给定的情况下背包能够装入的物品的最大价值

    我们首先考虑一个能够产生子问题的选择方式,这个选择方式需要能够获得一个最优解。通俗一点,即选择一个解决问题A的方法,其需要先解决子问题(的集合)B,而在确定B能够获得最优解的情况下保证A也能获得最优解。保持子问题尽可能简单而避免扩展(即尽可能不要1生2、2生4,而应当是1生1)。

    在背包问题中,我们假设从第一个物品按顺序选到第n个物品,而最终解决的问题就是在决定是否选择第n个物品时(W)容量所能承载的最大价值的问题(设为Final Problem,FP)。而为了解决FP,我们做出选择,从而将FP分解为子问题:

    • 决定是否选择第n-1个物品时(W)容量所能承载的最大价值(F_{n-1,W})

    • 决定是否选择第n-1个物品时(W-w_n)所能承载的最大价值(F_{n-1,W-w_n})

    [F_{n,W} = max(F_{n-1,W},F_{n-1,W-w_n} + v_n) ]

    我们采用记忆化搜索的思想,将(dp[i][j])作为第(i)个物品时容量最大为W的最大价值,每次需要重新计算时,先查一次记忆化表,从而避免子问题的二次计算。

    在这里我们考虑利用滚动数组优化。滚动数组是动态规划问题中常用的优化方式,主要用于优化空间复杂度。假设我们开一个二维数组,分别对应第(i)个物品和当前容量(j),那么实际上我们会发现,(F_{n,W})实际上只需要(F_{n-1,W})(F_{n-1,W-w_n})就能得出,也就是说,(F_{n-2到0})都在最终计算时用不上,那么我们就可以将这个二维数组压缩到一维,每次都复用数组空间来实现压缩空间的目的,这就是滚动数组的基本思想:通过反复运用同一块空间来实现数组的“滚动”。

    因此优化后我们可以得到:

    [F_W = max(F_W,F_{W-w_i}+v_n) ]

    以上就是状态转移方程的推导

    注意到这个方程是几乎所有背包问题的基础

    这里需要注意一点,等式左侧的(F_W)实际上对应选取第n个物品时容量为W的最大价值,而右侧的(F_W)对应的是取第n-1个物品时容量为W的最大价值,由于滚动数组,这两个变量共享了同一块地址空间,因此说明了在物品选择的维度上我们需要从0到n递增

    下面我们来看一个代码,从而理解物品容量的维度上我们应该如何处理

    for(int i=0;i<n;i++){
        for(int j=0;j<=W-w[i];j++){
            f[j+w[i]] = max(f[j]+v[i],f[j+w[i]]);
        }
    }
    

    这个代码在W的求解顺序上有问题,为什么呢?

    大家考虑在固定(i=x)的情况下,说明我们正在研究第x个物品,那么我们发现,在求取(W)的维度时,我们先求取了f[0]、f[1]……,这导致了什么问题呢?我们发现(f[w])需要基于(f[w-w[i]])而得出,逻辑上代表了(f[w-w[i]])的最大价值加上第x个物品的价值,而(f[w-w[i]])是在(f[w])前求取的,那么(f[w-w[i]])实际上是(f[w-w[i]-w[i]])的最大价值加上第x个物品的价值,你会发现第x个物品在求取f[w]时已经被放进去多次!,这与题目的0-1性质相悖。

    实际上,多重背包问题的解法就是这样的顺序

    正确的顺序应该是:

    for(int i=0;i<n;i++){
        for(int j=W;j>=w[i];j--){
            f[j] = max(f[j-w[i]]+v[i],f[j);
        }
    }
    

    完全背包

    我们更近一步,考虑一个物品可能可以选择无限次,得到完全背包问题。

    在获取第(i)个物品时容量为(j)对应的最大价值时,要考虑上一个物品时容量为(j)时最大价值和考虑第i个物品时容量为(j-w_i)时的最大价值。

    [F_{i,j} = max(F_{i-1,j},F_{i,j-w_i}+v_i) ]

    再次考虑滚动数组优化,可见(F_i)(F_{i-1})得出,而与其之前的无关,因此可以把物品个数的维度去掉,使其优化为一维的空间。

    [F_{j} = max(F_{j},F_{j-w_i}+v_i) ]

    注意这里和0-1问题不同,在考虑第(i)个物品时,需要从容量为0时逐个考虑将第(i)个物品装入的价值,因此求取顺序上与0-1不同。此处按下不表、

    多重背包

    多重背包问题中,每个物品不再能无限次选取,而只能最多选取(k_i)次。我们当然可以将其看为(k_i)个相同物品,然后和0-1背包问题一样看待,这样的复杂度为(O(nWmax(k_i)))

    然后考虑优化问题,我们可以注意到,对三个完全一致的物品ABC,同时选AB和同时选BC是一样的,但是我们将其视为两种不同的子问题进行了求解,因此如何解决这种重复求解问题是优化的关键。

    如何将(k_{i})个相同的物品进行拆分,使得不需要对每个物品单独考虑子问题空间,是一个有效的优化想法。而考虑对不定数量物品拆分,可以考虑二进制分组优化。二进制分组优化的核心在于,任意一个整数n,都能拆分成2的整数幂的和与一个余数的集合,而对任意i<=n,i都能表达为这个集合子集的和

    因此我们将(k_i)拆分为1+2+4+8+……和一个余数,使得每1、2、4、8……个物品整合成为一个“大物品”,然后对这些大物品组合成的物品列表考虑0-1背包问题即可。

    index = 0;
    for (int i = 1; i <= m; i++) {
      int c = 1, p, h, k; // k个重量为p、价值为h的物品
      cin >> p >> h >> k;
      while (k - c > 0) {
        k -= c;
        list[++index].w = c * p;
        list[index].v = c * h;
        c *= 2;
      }
      list[++index].w = p * k;
      list[index].v = h * k;
    } // 二进制分组的逻辑代码
    

    混合背包

    前面三种背包问题的缝合怪,此处按下不表。

    二维费用背包

    当考虑的物品不仅具有重量这一个限制条件,还含有价格这一新的限制条件,求取容量和经费均有限的情况下的最大价值。

    实际上就是多了层循环,注意体会这个转换(以下以0-1背包问题展示):

    for(int i=0;i<=n;i++){
    	for(int money=max_money;money >= money_i;money++){
    		for(int cap=max_cap;cap>=weight_i;cap++){
    			f[money][cap]=max(f[money][cap],
                 f[money-money_i][cap-weight_i]+v[i]);
    		}
    	}
    }
    
  • 相关阅读:
    封装TensorFlow神经网络
    android对话框显示异常报错:You need to use a Theme.AppCompat theme (or descendant) with this activity.
    管道过滤器模式
    架构设计模式之管道——过滤器模式
    SQL SERVER 数据库邮件配置
    浅谈数据仓库的基本架构(转)
    Spark On YARN内存分配
    Tomcat 9.0安装配置
    Spark on Yarn遇到的几个问题
    yarn资源调度(网络搜集)
  • 原文地址:https://www.cnblogs.com/Nortonary/p/14572221.html
Copyright © 2011-2022 走看看