动态规划——背包问题

    背包问题是一类非常典型的动态规划问题,包括多种类型(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);

参考

《背包九讲》

原文地址:https://www.cnblogs.com/gtarcoder/p/4840024.html