背包问题(二)-完全背包-史上最详细解答

  • Post author:
  • Post category:其他




背包问题(二)-完全背包-史上最详细解答





1. 题目

  • 问题描述:有n件物品和容量为m的背包,给出i件物品的重量以及价值value,还有数量number,求解让装入背包的物品重量不超过背包容量W,且价值V最大 。
  • 特点:题干看似与01一样,但它的特点是每个物品可以无限选用。



2. 分析



2.1 状态表示

一般用dp数组来计算动态规划问题,从以下两个方面对动态规划问题进行表示

  • 集合

    • v集合:物品价值
    • w集合:物品重量
    • 从前i个物品里面选取总重量<=j的所有物品的选法,

      与01背包的区别在于,每一种物品是可以无限选择的
  • 属性

    • max
    • min
    • count

本题属性是属于求最大价值,为max



2.2 状态计算

对于完全背包的问题,遵从01背包的策略,是选择放或者不放两个状态,但是每一种物品可以放无限个,

因此可以转换为:实际上我们对于一个物品的选择就是放多少个的问题



我们假设一种物品选择k个(

因为背包本身是有重量限制的,所以是k个而不是无限个

  • 选择放进去

    如果选择放进去,还需要考虑放进去多少个,即:

    1, 2, 3, ···, k-1, k个

    表示在上一个物品的状态的时候,我的当前背包重量

    j

    需要减去当前

    k

    个物品的重量

    k*w[i]

    ,并且整个背包的价值需要加上当前

    k

    个物品的价值

    k*v[i]

    ,则状态方程为:
dp[i][j] = dp[i-1][j-k*w[i]] + k*v[i]
  • 选择不放进去

    实际上如果选择不放进去的时候,表示

    放进去的是0个,需要减去的k

    w[i]和需要加上的k

    v[i]都为0

    选择不放进去的状态方程则为:
# dp[i][j] = dp[i-1][j-0*w[i]] + 0*v[i]
dp[i][j] = dp[i-1][j]

由此我们可以得到状态转移方程:

dp[i][j] = max(dp[i-1][j-k*w[i]] + k*v[i], dp[i-1][j])



3. 实现

根据上面的状态转移方程我们可以得到完全背包的解法:

def _complete_two_dim_k_function(data, number, total_weight):
    # 由于数据从1开始计算因此+1
    row = number + 1
    col = total_weight + 1
    # dp = [[0] * col for _ in range(row)]
    dp = np.array([0] * (row * col)).reshape(row, col)
    for i in range(1, row):
        if i == len(data):
            break
        item = data[i]
        v = item.get("value")
        w = item.get("weight")
        for j in range(1, col):
            for k in range(1, j):
                if j > k * w:
                    input_val = dp[i - 1][j - k*w] + k*v
                    noput_val = dp[i - 1][j]
                    dp[i][j] = max(input_val, noput_val)
                else:
                    dp[i][j] = dp[i - 1][j]
    return dp[number-1][total_weight]



4. 优化



4.1 去除k循环(时间复杂度优化)



4.1.1 数学推算

【1】

其实在上面的三层循环是一个圈复杂度比较高的代码,我们观察上述的状态方程:

# 选择不放进去
dp[i][j] = dp[i-1][j]

# 选择放进去
dp[i][j] = dp[i-1][j-k*w[i]] + k*v[i]

k循环其实是为了寻找当前第i轮物品,选取多少个能够达到最优的值的问题,即【式子1】:

# 0 < k & k*w[i] <= j
dp[i-1][j-k*w[i]] + k*v[i] = max{
	dp[i-1][j-1*w[i]] + 1*v[i],
	dp[i-1][j-2*w[i]] + 2*v[i],
	···
	dp[i-1][j-(k-1)*w[i]] + (k-1)*v[i],
	dp[i-1][j-k*w[i]] + k*v[i],
}

整理上面的式子,

使得整体减掉一个v[i]

可得【式子2】:

# 1 < k & k*w[i] <= j
dp[i-1][j-k*w[i]] + (k-1)*v[i] = max{
	dp[i-1][j-w[i]] + 0*v[i],
	dp[i-1][j-2*w[i]] + 1*v[i],
	dp[i-1][j-3*w[i]] + 2*v[i],
	···
	dp[i-1][j-(k-1)*w[i]] + (k-2)*v[i],
	dp[i-1][j-k*w[i]] + (k-1)*v[i]
}

我们设dp[i-1][j-k*w[i]] + (k-1)*v[i]得到得最大值为:

Z


则原先的状态转移方程则可以表示为:

dp[i][j] = max(Z+v[i], dp[i-1][j])

从上述的【式子2】中,我们单独的把“j-w[i]”看做一个整体,比做大写的:

X

,可以得到【式子3】:

# 0 < (k-1) & k*w[i] <= j
X = j-w[i]
dp[i][j] = dp[i-1][j-k*w[i]] + k*v[i] = max{
	dp[i-1][X-0*w[i]] + 0*v[i],
	dp[i-1][X-1*w[i]] + 1*v[i],
	dp[i-1][X-3*w[i]] + 2*v[i],
	···
	dp[i-1][X-(k-1)*w[i]] + (k-1)*v[i]  # 此处最多只能到k-1个
} + v[i]

对于单独的表示dp[i][j-w[i]],是【式子4】:

# 0 < k & (k+1)*w[i] <= j
dp[i][X] = dp[i-1][X-k*w[i]] + k*v[i]  # 即上述【式子4】中的max部分

可以看到【式子3】的k范围是: 0 < (k-1) & k*w[i] <= j

我们使

大写K = k-1


则有【式子5】:

# 0 < (k-1) & k*w[i] <= j
K = k - 1
# 1 < k & (k+1)*w[i] <= j
dp[i][j] = dp[i-1][j-k*w[i]] + k*v[i] = max{
	dp[i-1][X-0*w[i]] + 0*v[i],
	dp[i-1][X-1*w[i]] + 1*v[i],
	dp[i-1][X-3*w[i]] + 2*v[i],
	···
	dp[i-1][X-K*w[i]] + K*v[i]
} + v[i] = dp[i][X] + v[i]

则【式子4】和【式子5】则可以得到

dp[i][j-w[i]]选k-1个与dp[i][j]选k个的结果是完全一样的

因此可以得到,实际上我们赋值dp[i][j]的时候只需要从“dp[i][j-w[i]]+v[i]”和“dp[i-1][j]”中选择即可,即优化后的状态转移方程:

dp[i][j] = max(dp[i-1][j], dp[i][j-w[i]]+v[i])



4.1.2 代码实现

根据上述的新的状态转移方程,可以得到新的实现:

def _complete_two_dim_function(data, number, total_weight):
    """
    时间复杂度优化,去除了k循环
    状态转移方程:
    dp[i][j] = max(dp[i][j-w[i]] + v[i], dp[i-1][j])
    :param data:
    :param number:
    :param total_weight:
    :return:
    """
    # 由于数据从1开始计算因此+1
    row = number + 1
    col = total_weight + 1
    # dp = [[0] * col for _ in range(row)]
    dp = np.array([0] * (row * col)).reshape(row, col)
    for i in range(1, row):
        if i == len(data):
            break
        item = data[i]
        v = item.get("value")
        w = item.get("weight")
        for j in range(w, col):
            if j >= w:
                input_val = dp[i][j - w] + v
                noput_val = dp[i - 1][j]
                dp[i][j] = max(input_val, noput_val)
            else:
                dp[i][j] = dp[i - 1][j]
                break  # 如果w >= j了这个循环就没必要继续了
    return dp[number - 1][total_weight]



4.2 转化成一维数组解法(空间复杂度优化)

和01背包的优化逻辑一样,

i

这个变量其实就是表示“第i个”的一个递增序列,实际的这个

背包的当前的状态只有重量(w)和价值(v)


根据刚才的状态方程:

# 不放进去
dp[i][j]=dp[i-1][j]

# 放进去
dp[i][j]=dp[i][j-w[i]] + v[i]

观察两个状态方程,可以看到对于背包重量的状态

j

是与

i

无关,因此可以把上述方程简化为:

# 不放进去时候,重量不变,价值不变
dp[j] = dp[j] 

# 放进去的时候,背包重量和价值的变化
dp[j] = dp[j-w[i]] + v[i]

因此可以得到状态转移方程为:

dp[j] = max(dp[j-w[i]] + v[i], dp[j])

根据上述的状态转移方程来实现代码:

def _complete_one_dim_function(data, number, total_weight):
    """
    状态转移方程:
    i:表示第几个物品
    k:表示重量几许
    dp[k] = max(value[i]+dp[k-weight[i]], dp[k])
    :param data:
    :param number:
    :param total_weight:
    :return:
    """
    # 由于数据从1开始计算因此+1
    row = number + 1
    col = total_weight + 1
    dp = np.array([0] * col)
    for i in range(1, row):
        if i == len(data):
            break
        item = data[i]
        # 这个地方需要正序列遍历,原因是因为完全背包算法实际上是需要进行自我比对的
        # 也就是说我当前的这一轮可以被重复,也就是说一个物品可以放进去多次
        v = item.get("value")
        w = item.get("weight")
        for j in range(w, col):
            dp[j] = max(dp[j - w] + v, dp[j])
    return dp[total_weight]

此处为何与01背包不同(

01背包链接

),是

正序列遍历j


首先我们观察优化后和优化前的状态转移方程:

# 优化之前
dp[i][j] = max(dp[i][j-w[i]] + v[i], dp[i-1][j])

# 优化之后
dp[j] = max(dp[j-w[i]] + v[i], dp[j])

因此实际上优化后的状态转移方程是:

dp[j](第i轮的新值) = max(dp[j-w[i]] + v[i](第i轮的新值), dp[j](第i-1轮的旧值))

对比01背包的状态转移方程

【2】

dp[j](第i轮的新值) = max(dp[j-w[i]] + v[i](第i-1轮的旧值), dp[j](第i-1轮的旧值))

我们从01背包中可以知道,想要保证每个物品只被使用一次,就要保证每一次与

第i轮

比较的数据都是第

第i-1轮

的旧数据。因此在01背包中需要逆序。

但是在完全背包中,需要与当前

第i轮

比较的数据也是

第i轮

的,这就刚好使用正序遍历即可。原因是

j-w[i]

是做减法的,而这个

j

又是数组的下标,做

减法

之后就表示是之前的数据。因此此处正序遍历,这个数据就已经是

第i轮

更新的数据与

第i轮

的数据进行比较了。



5. 测试

我们给出01背包的测试数据

{
	"things_num": 10,
	"items": [{
		"value": 86,
		"number": 999999,
		"weight": 28
	}, {
		"value": 553,
		"number": 999999,
		"weight": 7
	}, {
		"value": 27,
		"number": 999999,
		"weight": 29
	}, {
		"value": 246,
		"number": 999999,
		"weight": 33
	}, {
		"value": 55,
		"number": 999999,
		"weight": 62
	}, {
		"value": 403,
		"number": 999999,
		"weight": 77
	}, {
		"value": 234,
		"number": 999999,
		"weight": 19
	}, {
		"value": 216,
		"number": 999999,
		"weight": 30
	}, {
		"value": 481,
		"number": 999999,
		"weight": 1
	}, {
		"value": 387,
		"number": 999999,
		"weight": 42
	}],
	"total_weight": 331
}

输出:

159211



参考链接


【1】时间复杂度优化: https://blog.csdn.net/ACM_hades/article/details/89190424


【2】01背包详解:https://blog.csdn.net/aninstein/article/details/108061603



下一章:

背包问题(三)-多重背包(简单)-史上最详细解答



版权声明:本文为aninstein原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。