多重背包可行性问题

首先介绍一下:什么是多重背包问题?背包问题?

0-1背包问题

  1. 问题描述

给定 N = [ N 1 , N 2 , . . . , N N ] N=[N_1, N_2, ..., N_N] N=[N1,N2,...,NN]件物品以及每件物品对应的重量 W = [ W 1 , W 2 , . . . , W N ] W=[W_1, W_2, ..., W_N] W=[W1,W2,...,WN]和价值 V = [ V 1 , V 2 , . . . , V N ] V=[V_1, V_2, ..., V_N] V=[V1,V2,...,VN],求解将这些物品放入容量为 C C C的背包得到的最大价值为多少?

注意:每种物品仅有一件,多重背包问题此处定义不一致。

  1. 求解思路

采用动态规划的思想求解该问题,定义子问题及其状态转移。

特点:每种物品仅有一件,可以选择放或者不放入背包。

用子问题定义状态: F ( i , c ) F(i,c) F(i,c)表示前 i i i件物品放入容量为 c c c的背包可以获得的最大价值。

选择将第 i i i件物品放入背包得到的价值:

F = F ( i − 1 , c − W i ) + V i F = F(i-1, c-W_i) + V_i F=F(i1,cWi)+Vi

选择将第 i i i件物品放入背包得到的价值:

F = F ( i − 1 , c ) F = F(i-1, c) F=F(i1,c)

最后对两者求最大值即可:

F ( i , c ) = m a x ( F ( i − 1 , c ) , F ( i − 1 , c − W i ) + V i ) F(i, c) = max(F(i-1, c), F(i-1, c-W_i) + V_i) F(i,c)=max(F(i1,c),F(i1,cWi)+Vi)

时间复杂度: O ( N ∗ C ) O(N*C) O(NC)

空间复杂度: O ( N ∗ C ) O(N*C) O(NC)

  1. 优化空间复杂度

    假设给定物品详情如下:

    输入:物品件数:3,背包容量:5;

    每件物品的重量: 1, 2, 3;

    每件物品的价值:6, 10, 12.

    (1)将 O ( N ∗ C ) O(N*C) O(NC)的空间复杂度优化到 O ( 2 ∗ C ) O(2*C) O(2C)的空间复杂度:

    根据递推表达式,知道每次递推第 i i i的时候都只用到了第 i − 1 i-1 i1项的值,那么能否只用两行来表示整个全部的递推过程呢?

    针对上述例子,采用 O ( N ∗ C ) O(N*C) O(NC)的空间复杂度的递推状态结果如下:

    物品件数/容量 0 1 2 3 4 5
    i = 0 i=0 i=0 0 6 6 6 6 6
    i = 1 i=1 i=1 0 6 10 16 16 16
    i = 2 i=2 i=2 0 6 10 16 18 22
    def zero_one_pack(weight, value, N, C):
    
        assert len(weight) == len(value)
        assert N >= 0
    
        memo = []
        for i in range(N):
            memo.append([-1] * (C+1))
    
        for i in range(C + 1):
            memo[0][i] = value[0] if i >= weight[0] else 0
    
        for i in range(1, N):
            for j in range(C+1):
                memo[i][j] = memo[i - 1][j]
                if j >= weight[i]:
                    memo[i][j] = max(memo[i][j], memo[i-1][j - weight[i]] + value[i])
    
        return memo[N-1][C]
    

    采取两行的递推结果如下:

    第一步:

    物品件数/容量 0 1 2 3 4 5
    i = 0 i=0 i=0 0 6 6 6 6 6
    i = 1 i=1 i=1 0 6 10 16 16 16

    第二步:

    物品件数/容量 0 1 2 3 4 5
    i = 2 i=2 i=2 0 6 10 16 18 22
    i = 1 i=1 i=1 0 6 10 16 16 16

    当物品数量较多的时候,依次类推,可以发现当 i i i为偶数的时候,更新值都位于第一行,当 i i i为奇数的时候,更新值都位于第二行。

    def one_zero_pack_second(weight, value, N, C):
        assert len(weight) == len(value)
        assert N >= 0
    
        memo = []
        for i in range(2):
            memo.append([-1] * (C + 1))
    
        for i in range(C+1):
            memo[0][i] = value[0] if i >= weight[0] else 0
    
        n_index = 1
        while n_index < N:
            for j in range(C+1):
                # n_index & 1 == 0 表示n_index为偶数,n_index & 1 == 1表示n_index为奇数
                memo[n_index & 1][j] = memo[(n_index-1) & 1][j]
                if j >= weight[n_index]:
                    memo[n_index & 1][j] = max(memo[n_index & 1][j], value[n_index] + memo[(n_index-1) & 1][j - weight[n_index]])
    
            n_index += 1
    
        return memo[(N-1) & 1][C]
    

    (2)将 O ( 2 ∗ C ) O(2 *C) O(2C)的空间复杂度优化到 O ( C ) O(C) O(C)的空间复杂度

    在递推 F ( i , c ) F(i, c) F(i,c)的时候只用到了 i − 1 i-1 i1行的数据,然而真正用到的并不是 i − 1 i-1 i1行的全部数据,而是 i − 1 i-1 i1行的某一个值 F ( i − 1 , c ) F(i-1, c) F(i1,c)或者 F ( i − 1 , c − W i ) F(i-1, c-W_i) F(i1,cWi),能否在更新的时候,只对一行数据进行逆向更新呢,先更新 F ( i , C ) F(i, C) F(i,C),再更新 F ( i , C − 1 ) F(i, C-1) F(i,C1),依次往前递推,前边值的更新并不需要用到后边的数据,而前向更新的时候,更新后边的值可能用到前边的数据.

    构建递推表达式:
    F ( c ) = m a x ( F ( c ) , F ( c − W i ) + V i ) F(c) = max(F(c), F(c-W_i)+V_i) F(c)=max(F(c),F(cWi)+Vi)

    接着上边的例子:

    第一步初始化:

    物品件数/容量 0 1 2 3 4 5
    i = 0 i=0 i=0 0 6 6 6 6 6

    第二步更新:

    物品件数/容量 0 1 2 3 4 5
    i = 1 i=1 i=1 0 6 10 16 16 16

    第三步更新:

    物品件数/容量 0 1 2 3 4 5
    i = 2 i=2 i=2 0 6 10 16 18 22
    def one_zero_pack_third(weight, value, N, C):
        assert len(weight) == len(value)
        assert N >= 0
    
        memo = []
        for i in range(C+1):
            memo.append(value[0] if i >= weight[0] else 0)
    
        for i in range(1, N):
            # 注意: 这个地方j从 C开始递推
            for j in range(C, weight[i] - 1, -1):
                memo[j] = max(memo[j], value[i] + memo[j - weight[i]])
    
        return memo[C]
    

多重背包问题

多重背包问题是在0-1背包问题的基础上进行升华的一个问题,0-1背包问题只限定每个物品只有一个,多重背包问题对于每个物品具有指定数目个,引入numbers变量, n u m b e r s = [ M 1 , M 2 , M 3 , . . . , M N ] numbers=[M_1, M_2, M_3, ..., M_N] numbers=[M1,M2,M3,...,MN]

多重背包问题的求解方案:

  1. 将每个物品的多件,拆成多个单件物品,该问题变成了原来的0-1背包问题,时间复杂度为 O ( C ∗ ∑ i M i ) O(C *\sum_i M_i) O(CiMi)

  2. 将第 i i i件物品转换成若干件原始物品的和,使得原问题中的第 i i i种物品的可取策略为 [ 0 , 2 0 , 2 1 , . . . , 2 k − 1 , M i − 2 k + 1 ] [0, 2^0, 2^1, ..., 2^{k-1},M_i - 2^k + 1] [0,20,21,...,2k1,Mi2k+1] k k k是满足 M i − 2 k + 1 > 0 M_i - 2^k + 1>0 Mi2k+1>0的最大的整数,例如:当 M i = 13 M_i=13 Mi=13时, k = 3 k=3 k=3,此时时间复杂度为: O ( C ∗ ∑ i l o g ( M i ) ) O(C * \sum_i log(M_i)) O(Cilog(Mi))

def multi_pack(weight, value, nums, N, C):

    c_i = 0
    len_capa = N
    while c_i < len_capa:
        # 将费用大于最大容量的物品剔除掉
        if weight[c_i] > C:
            weight.pop(c_i)
            value.pop(c_i)
            len_capa -= 1
            continue
        c_i += 1

    new_capacity = []
    new_value = []

    for i in range(len(weight)):

        k = int(math.log(nums[i] + 1, 2))
        if round(math.log(nums[i] + 1, 2) - k, 1) == 0.0:
            k = k - 1

        for j in range(k):
            new_capacity.append(weight[i] * pow(2, j))
            new_value.append(value[i] * pow(2, j))

        if (nums[i] - pow(2, k) + 1) != 0:
            new_capacity.append((nums[i] - pow(2, k) + 1) * weight[i])
            new_value.append((nums[i] - pow(2, k) + 1) * value[i])

    res = one_zero_pack_third(new_capacity, new_value, len(new_capacity), C)
    return res

多重背包可行性问题

多重背包可行性问题指的是:每种有若干件的物品能否填满给定容量的背包,此时不考虑价值最大问题

实际案例:

小明身边有2张1元现金,3张5元现金,4张20元现金,1张50元现金,问小明身边能够凑足19元整钱吗?

即用:numbers = [2, 3, 4, 1], weight = [1, 5, 20,50]的物品去填充19元的背包,求解是否可行?

使用动态规划求解该问题,定义子问题的状态转移:

F ( i , j ) F(i, j) F(i,j)表示使用前 i i i个物品,填充容量为 j j j的背包,第 i i i个物品最多能够剩余多少个,如果无法填充容量为 j j j的背包,则值为-1.

根据该定义,可以得到如下递推表达式:
F ( i , j ) = { M i , F ( i − 1 , j ) ≥ 0 − 1 , j < W i o r F ( i , j − W i ) ≤ 0 F ( i , j − W i ) − 1 , 其 他 F(i,j)=\left\{ \begin{aligned} M_i, \qquad \qquad \qquad \qquad \quad F(i-1, j) \geq 0\\ -1, \qquad j<W_i \quad or \quad F(i, j-W_i) \leq 0 \\ F(i, j-W_i) - 1, \qquad \qquad \qquad \qquad 其他 \end{aligned} \right. F(i,j)=Mi,F(i1,j)01,j<WiorF(i,jWi)0F(i,jWi)1,

# 背包问题---可行性
def can_fill_pack(capacity, nums, C):

    len_capa = len(capacity)
    F = []
    for i in range(len_capa):
        F.append([])
        for j in range(C + 1):
            F[i].append(-1)

    k = min(int(C / capacity[0]), nums[i])
    for i in range(k + 1):
        F[0][k * capacity[0]] = nums[i] - k

    for i in range(1, len_capa):
        for j in range(C + 1):
            if F[i - 1][j] >= 0:
                F[i][j] = nums[i]
            elif j < capacity[i] or F[i][j - capacity[i]] <= 0:
                F[i][j] = -1
            else:
                F[i][j] = F[i][j - capacity[i]] - 1

    if F[len_capa - 1][C] != -1:
        return True
    else:
        return False

由于每一次都只用到了一行的数据,故而可以对空间复杂度进行缩减.

def can_fill_pack_second(weight, nums, C):
    len_capa = len(weight)

    F = []

    for i in range(C + 1):
        F.append(-1)

    k = min(int(C / weight[0]), nums[0])
    for i in range(k + 1):
        F[k * weight[0]] = nums[i] - k

    for i in range(1, len_capa):
        for j in range(C + 1):
            if F[j] >= 0:
                F[j] = nums[i]
            elif j < weight[i] or F[j - weight[i]] <= 0:
                F[j] = -1
            else:
                F[j] = F[j - weight[i]] - 1

    if F[C] != -1:
        return True
    else:
        return False

上述代码均通过小样本测试用例,如若描述有误之处,还望指正!

参考资料:

[1]. https://github.com/tianyicui/pack

[2].https://blog.csdn.net/ctsas/article/details/53708712

你可能感兴趣的:(数据结构与算法)