zoukankan      html  css  js  c++  java
  • 动态规划——背包问题

        背包问题是一类非常典型的动态规划问题,包括多种类型(01背包、完全背包、多重背包、混合背包、二维费用背包等)其基本类型为01背包问题。

    一、01背包问题

    N件物品,每件物品的重量和价值分别为 w[i], v[i], 把这些物品放到一个容量为W的背包中,求背包中物品的价值的最大值。

    形式化定义: 

    分析 
        最直观的思路是枚举背包中出现的所有可能的物品组合,然后计算它们的价值和,求最大值。如果直接采用递归搜索,则会有大量的冗余,通过记忆化搜索的方式可以进行改进(记忆 max[i][w], 前i中物品总重量达到w时候的最大价值,其中i和w又是递归函数的参数)。通过动态规划方式可以进一步改进。

        动态规划要考虑最优子结构和无后效性,对于该问题,假设前i-1种物品在重量和约束W下得到了一个最大价值和M,那么在考虑前i种物品在重量和约束W'下价值的最大值M'时,我们只关心前i-1种物品的重量和W和最大价值和M,至于这前i-1种物品时如何选择对求解M'无影响(因为M' = M + v[i],假设W + w[i] = W'),即无后效性;同时,前i种物品在重量约束W'下得到了价值的最大值,此时选取的物品集合为S,那么S中必然包含前i-1种物品在重量约束W(W = W'或W = W'-w[i])下得到最大值的组合(用反证法可以证明),即最优子结构。

        因此考虑将问题规定为前i种物品总重量不超过w时,能够得到的最大价值 f[i][w]。此时有递推公式

        f[i][w] = max(f[i-1][w], f[i-1][w - w[i]] + v[i])
        //情形1.不选择第i种物品,那么前i种物品可得到的最大值为 f[i-1][w];
        //情形2.选择第i种物品,那么前i-1种物品可以使用 w -w[i]的空间存放,前i-1种物品可得最大价值 f[i-1][w-w[i]], 再加上第i种物品的价值 v[i]
        //在二者中间取最大值即可。
    

        针对实际问题设置边界条件 
    显然i>=0, w>=0. f[0][w]表示不选任何物品时,总重量不超过w,取得的价值和的最大值,显然为0,即f[0][w] = 0; f[i][0] 表示前i中物品,总重量和不超过0,取得价值和的最大值,由于问题中重量均为正整数,因此,显然为0,即 f[i][0] = 0.

    优化 
        可以看出,f[i][xx]只和f[i-1][yy]有关,假设当前有一个一维数组,存放了前i-1种物品在某约束xx下的最大值,则可以直接求出前i种物品在某约束yy下的最大值。因此可以使用一维数组,来滚动实现二维数组,进行空间优化(通过画图更直观一些)。 
        f[i][w] = max(f[i-1][w], f[i-1][w - w[i]] + v[i]),改变为一维数组 f[w] = max(f[w], f[w-w[i]] + v[i]),在一维数组中考虑,重量w只和w及w-w[i]有关,因此要从后向前进行递推(若从前向后,则会导致被修改值x之后的值都是在新的x基础上(即前i种物品选择)被修改,不符合在前i-1种物品中进行选择的要求。

    实现

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

    复杂度分析 
        经过空间优化之后,空间复杂度为O(W), 时间复杂度为O(n*w).

    二、完全背包问题

        有N种物品,每种物品有个重量w[i]和价值v[i],每种物品可以选择无限多件。给定一个承重不超过 W的背包,求出背包中所放物品的价值和的最大值。

        和01背包问题一样分析,设 f[i][w]表示前i种物品中选择总重不超过w的物品所能得到的价值和的最大值。有

    f[i][w] = max{f[i-1][w], f[i][w - k*w[i]] + k*v[i]} | k属于[1, w/w[i]]
    

    分析

        观察f[i][w] = max{f[i-1][w - k*w[i]] + k*v[i]} | k属于[0, w/w[i]],
        max{f[i-1][w - k*w[i]] + k*v[i]}
        = max{f[i-1][w], f[i-1][w-k*w[i]] + k*v[i]} | k属于[1, w/w[i]],
        令k = t+1, 则
        max{f[i-1][w-k*w[i]] + k*v[i]} | k属于[1, w/w[i]],
        = max{f[i-1][w-w[i] - t*w[i]] + t*v[i]} + v[i] | t属于[0, (w-w[i])/w[i]],
        = f[i-1][w - w[i]] + v[i]
        因此
        f[i][w] = max{f[i-1][w], f[i][w-w[i]] + v[i]}
    

    因此递推公式化简为了 f[i][w] = max{f[i-1][w], f[i][w-w[i]] + v[i]} 
        对于这个公式,其实可以这样理解:在求f[i][w]需要求 max{f[i-1][w - k*w[i]] + k*v[i]}时候,而在求f[i][w-w[i]]时,已经求了max{f[i-1][w - k*w[i]] + k*v[i]}中的前面若干项,因此,知道了 f[i-1][w-w[i]]时,就可以继续推出f[i][w],而不需要再重复求max{f[i-1][w - k*w[i]] + k*v[i]}。

        同样,可以利用一维数组进行空间优化,得到递推公式 
    f[w] = max{f[w], f[w-w[i]] + v[i]

    实现

    for(int i = 0; i <= W; i ++){
        f[w] = 0;
    }
    for(int i = 0; i <= n; i ++){ 
        for(int w = w[i]; w <=  W; w ++){
        //这里递推从左向右!!因为就是需要前面的值更新对后面的值产生影响
            f[w] = max{f[w], f[w - w[i]] + v[i]};
        }
    }
    

    复杂度分析 
        在经过空间优化之后,空间复杂度为O(W),时间复杂度为O(n*W)

    对比01背包和完全背包 
        都可以使用一维数组来降低空间复杂度,使用一维数组时,01背包的w顺序为从后向前(w ~ w - w[i]),而完全背包是从前向后(w-w[i] ~ w)。

    三、多重背包问题

        N种物品,第i种物品的重量值为w[i], 价值为v[i],且第i种物品有c[i]个。给定背包的承重W,求能放入背包中的物品总价值的最大值。

    分析 
        仿照01背包和完全背包,可以得到递推公式f[i][w] = max{f[i-1][w], f[i][w - k*w[i]] + k*v[i] | v属于[1, min(c[i], w/w[i])].
        可以将第i种物品的c[i]个视为不同种类的物品,只不过他们的重量和价值相同,此时转化为01背包问题,即从 n1c[i] 种物品,每种只有一件,选择总重量不超过W的物品组合,使得价值和最大。此时时间复杂度为 O(W*n1c[i]) 
        进一步优化,考虑一个定理

    给定一个正整数n,可以用1, 2,... 2^(k-1), n - 2^k + 1这些数(约log(n)个)来表示[1,n]中的任何一个整数,其中k为使得n - 2^k + 1 > 0的最大的正整数

    即二进制分解。。 
        利用这个定理,将c[i]个重量为w[i]、价值为v[i]的第i种物品分解为 {重量,价值} = {w[i], v[i]}, {2*w[i], 2*v[i]}, ...{2^(k-1)*w[i], 2^(k-1)*v[i]},{(c[i] - 2^k + 1)*w[i], (c[i] - 2^k + 1)v[i]}的一系列物品,然后转换为01背包问题。注意若存在新转换后的{w[k], v[k]}和原来的某种物品{w[j], v[j]}相同,不能将他们进行合并。
    此时时间复杂度为O(W*(n1log2(c[i]))。 
    实现

    int weight[MAX];
    int value[MAX];
    void Expand(int w, int v, int n, int& index){
        int k = 1;
        do{
            weight[index] = k*w;
            value[index ++] = k*v;
            k*=2;
        }while(k*2 < n);
        weight[index] = (n - k + 1)*w;
        value[index++] = (n - k + 1)*v;
    }
    

    四、混合背包问题

        混合背包问题是N种物品中有些物品是只能选一件或者不选,有些物品是可以选任意件,有些物品有个数限制。在背包承重W的限制下,求能够得到的价值总和的最大值。 
        解决方法是将多重背包转换为01背包,然后解决01背包和完全背包的混合。

    for(int i = 1; i <= n; i ++){
        if(物品i为01背包物品){
            for(int w = W; w >= w[i]; w --){
                f[w] = max{f[w], f[w - w[i]] + v[i]};
            }
        }else if(物品i为完全背包物品){
           for(int w = w[i]; w <= W; w ++){
                f[w] = max{f[w], f[w - w[i]] + v[i]};
            }
        }
    }
    

    五、二维费用的背包问题

        N中物品,每件物品i都有两种不同的空间消耗,选择这件物品必须同时付出这两种代价。对于每种代价都有一个可付出的最大值(背包容量),问怎样选择物品使得总价值和最大。

    分析 
        和一维约束没什么区别,只不过将状态表示增加一个维度, 
    f[i][w1][w2] = max{f[i-1][w1][w2], f[i-1][w1 - w1[i]][w2 - w2[i] + v[i]},利用空间优化之后为 f[w1][w2] = max{f[w1][w2], f[w1-w1[i]][w2-w2[i]] + v[i]},在递推的时候同样w1,w2从后向前。

    六、分组的背包问题

        有N件物品和一个容量为V的背包。第i件物品的费用是Ci,价值是Wi。这些物品被划分为K组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

    分析 
        这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设F [k, v]表示前k组物品花费费用v能取得的最大权值,则有:

    for k = 1 to K //组
        for w = W to 0  //承重
            for item i in group k   //组内的每个物品
                f[w] = max{f[w], f[w - Ci] + Wi}
    

    七、背包问题的相关问题

    7.1 输出选取物品方案

        如果不仅仅需要求出背包所承载物品的最大价值,还需要求出背包中选取的物品集合。则还需要维护另一个数组g[i][w], 若 f[i][w] == f[i-1][w]表示第i件物品没有被选择,则令g[i][w] = 0;若 f[i][w] == f[i-1[w - w[i]] + v[i],则说明选择了第i件物品,则令g[i][w] = 1,这样在求解f数组的时候顺便求出g数组。

    int i = n, w = W;
    while(i >= 1){
        if (g[i][w] == 0){
            未选择第i件物品
        }else{
            选择了第i件物品
            w = w - w[i]
        }
        i --;
    }
    
    7.2 输出字典序最小的最优方案

        字典序最小是指1...N号物品的选择方案排列出来以后字典序最小。一般而言,求一个字典序最小的最优方案,只需要在转移时注意策略。子问题的定义要修改一下:如果存在一个选了物品1的最优方案,那么答案一定包含物品1,原问题转化为一个背包容量为W-C1,物品为2....N的子问题。反之,如果答案不包括物品1,则转化成背包容量仍为W,物品为2....N的子问题。 
    令 f[i][w] 表示从物品i,i+1...N中选择若干物品总重量不超过w,所能得到价值的最大值。则有递推公式:f[i-1][w] = max{f[i][w], f[i][w-w[i-1]]},若f[i][w] == f[i][w-w[i-1]],则选择第i件物品。这样在递推完成之后,编号越小的物品就被记录下来,而同样优秀的字典序较大的方案被“覆盖”。

    7.3 最优方案总数
    g[0][0] = 1
    for(int i = 1; i <= N; i ++){
        for(int w = 0; w <= W; w ++){
            f[i][w] = max{f[i-1][w], f[i-1][w-w[i]]};
            g[i][w] = 0;
            if(f[i][w] == f[i-1][w]){
                g[i][w] += g[i-1][w];
            }
            if(f[i][w] == f[i-1][w-w[i]] + v[i]){
            //可能选择不选择第i种物品得到结果相同,这样方案总数要相加
                g[i][w] += g[i-1][w-w[i]];
            }
        }
    }
    
    7.4 求第k优解

    对于求次优解、第K优解类的问题,如果相应的最优解问题能写出状态转移方程、用动态规划解决,那么求次优解往往可以相同的复杂度解决,第K优解则比求最优解的复杂度上多一个系数K。

        基本思想是,将每个状态都表示成有序队列,将状态转移方程中的max/min转化成有序队列的合并。 
        用f[i][w][k]表示前i种物品中选择总重量不超过w的物品,得到的价值和的第k大的值,则可以推出 
    f[i][w][k] = Kth_Of_{f[i-1][w][1...k] + f[i-1][w-w[i]][1...k] + v[i]} 
        在两个大小为k的有序数组中,选择两个数组所有数字共同的第k大的数字。使用归并排序即可。

    for(int i = 1; i <= n; i ++){
        for(int w = w[i]; w <= W; w ++){
            int t = 0, n1 = 0, n2 = 0;
            while(t < k){
                if(f[i-1][w][n1] > f[i-1][w-w[i]][n2] + v[i]){
                    f[i][w][k] = f[i-1][w][n1++];
                }else{
                    k_th_q = 2;
                    f[i][w][k] = f[i-1][w - w[i]][n2 ++] + v[i];
                }
                t ++;   
           }
        }
    }
    //时间复杂度为O(nWk);
    

    参考

    《背包九讲》

  • 相关阅读:
    Python制作回合制手游外挂简单教程(中)
    软件工程知识大纲
    Android应用程序开发
    Python制作回合制手游外挂简单教程(上)
    操作系统概念大纲
    Java三种工厂模式
    Java泛型的理解
    Java动态代理的理解
    编译原理与技术大纲
    新服务器sudo与权限分配<NIOT>
  • 原文地址:https://www.cnblogs.com/gtarcoder/p/4840024.html
Copyright © 2011-2022 走看看