多背包问题的求解方法
在物流或资源分配的场景中,我们常常会遇到“多背包问题”,即如何将一批物品合理装入尽可能少的背包中。本文将探讨如何使用贪心算法结合整数规划和动态规划结合整数规划两种方法来解决这一问题,并对它们的效果进行对比。
问题描述
假设我们需要运送一批物品,每种物品的质量和需求数量如下:
- 质量: 26, 30, 40, 56, 60, 76, 80, 86, 90, 100, 110, 116, 120, 176, 180, 200
- 数量: 2, 10, 8, 16, 80, 6, 32, 4, 36, 4, 10, 8, 18, 40, 16, 8
我们的目标是求解出在背包承重为 600 的情况下,最少需要多少个背包才能装下所有物品。
方法一:贪心算法 + 整数规划
思路
贪心算法的核心在于局部最优,即每次尽可能将一个背包装满。通过贪心策略,我们先用整数规划求解出一个背包中可以装入的最大物品组合,然后重复这一过程直到所有物品都被装入。
- 贪心算法:
每次将背包最大程度的装满 - 整数规划:
数量, x 1 , x 2 , . . . , x 22 x_1,x_2,...,x_{22} x1,x2,...,x22
maximize: 26 ∗ x 1 + 29 ∗ x 2 + . . . + 201 ∗ x 22 26 * x_1 + 29 * x_2 + ... + 201 * x_{22} 26∗x1+29∗x2+...+201∗x22
S.T.:
26 ∗ x 1 + 29 ∗ x 2 + . . . + 201 ∗ x 22 ≤ 600 26 * x_1 + 29 * x_2 + ... + 201 * x_{22} \leq 600 26∗x1+29∗x2+...+201∗x22≤600
0 ≤ x 1 ≤ 2 , i n t e g e r 0 \leq x_1 \leq 2, integer 0≤x1≤2,integer
0 ≤ x 2 ≤ 10 , i n t e g e r 0 \leq x_2 \leq 10, integer 0≤x2≤10,integer
…
0 ≤ x 22 ≤ 8 , i n t e g e r 0 \leq x_{22} \leq 8, integer 0≤x22≤8,integer
实现步骤
- 对每个背包使用整数规划,计算出在不超过 600 重量限制的情况下,可以装入的最大物品数量。
- 重复上述步骤,直到所有物品都被装入背包。
优缺点
- 优点: 实现简单,求解速度快,适用于问题规模较小或对解的精度要求不高的场景。
- 缺点: 由于每次仅考虑局部最优解,最终可能需要更多的背包。
import numpy as np
import pulp as lp
def integer_program(weights, nums, max_weight):
"""
整数规划
input:
weights: 物品质量集
nums: 物品需求数量集
max_weight: 背包承重
output:
单个背包装入数量集
"""
prob = lp.LpProblem("The GY Problem", lp.LpMaximize)
x = []
for i, n in enumerate(nums):
x.append(lp.LpVariable("x_%03d" % i, lowBound=0, upBound=n, cat="Integer"))
prob += lp.lpSum([weights[i] * x[i] for i in range(len(x))]), "Total weight"
prob += lp.lpSum([weights[i] * x[i] for i in range(len(x))]) <= max_weight, "Max weight"
prob.solve()
# print(prob)
# print("\tStatus:", lp.LpStatus[prob.status])
return np.array([v.varValue for v in prob.variables()]).astype("int32")
# 初始化变量
weights_array = np.array(
[26, 30, 40, 56, 60, 76, 80, 86, 90, 100, 110, 116, 120, 176, 180, 200])
nums_array = np.array(
[2, 10, 8, 16, 80, 6, 32, 4, 36, 4, 10, 8, 18, 40, 16, 8])
bag_weight = 600
# 理论最少背包数
bags_num_min = np.ceil(np.sum(weights_array * nums_array) / bag_weight)
print("理论最少", bags_num_min)
# 贪心算法
bags_num = 0
bag_nums_list = [] # 每个背包装入物品方案集合
while np.any(nums_array > 0):
nums_array_ = integer_program(weights_array, nums_array, bag_weight)
nums_array -= nums_array_
bags_num += 1
bag_nums_list.append(nums_array_)
print("实际最少", bags_num)
理论最少 49.0
实际最少 51
最终,在这个案例中,贪心算法结合整数规划的结果为51个背包,虽然接近最优,但未必是全局最优解。
方法二:动态规划 + 整数规划
思路
动态规划的思想是先求出所有可能的“填满”背包的组合,然后通过整数规划从这些组合中找到最优解。这种方法确保了我们找到了全局最优的解决方案。
- 动态规划:
求解所有可能的背包装满方案(不能再放入任一物品)。 - 整数规划:
方案, c 1 , c 2 , . . . , c n c_1,c_2,...,c_n c1,c2,...,cn
每个方案采用数量, x 1 , x 2 , . . . , x n x_1,x_2,...,x_n x1,x2,...,xn
各个方案物品分布数量, A 1 i , A 2 i , . . . , A n i A_{1i},A_{2i},...,A_{ni} A1i,A2i,...,Ani
每种物品需求数量, b i bi bi
minimize: x 1 + x 2 + . . . + x n x_1 + x_2 + ... + x_n x1+x2+...+xn
S.T.:
A 1 i ∗ x 1 + A 2 i ∗ x 2 + . . . + A n i ∗ x 22 ≥ b i A_{1i} * x_1 + A_{2i} * x_2 + ... + A_{ni} * x_{22} \geq {bi} A1i∗x1+A2i∗x2+...+Ani∗x22≥bi
0 ≤ x 1 , i n t e g e r 0 \leq x_1, integer 0≤x1,integer
0 ≤ x 2 , i n t e g e r 0 \leq x_2, integer 0≤x2,integer
…
0 ≤ x n , i n t e g e r 0 \leq x_n, integer 0≤xn,integer
实现步骤
- 使用动态规划生成所有可能的“填满”背包的方案,即每个方案都是在背包不能再放入更多物品的情况下的组合。
- 使用整数规划确定如何组合这些方案,以最小化所需的背包数量。
优缺点
- 优点: 保证全局最优解,适用于对解的精度要求高的场景。
- 缺点: 计算复杂度高,求解时间较长。
import numpy as np
import pulp as lp
def bag_program(weights, item_counts, max_weight):
"""
动态规划,获取所有可能的背包装满方案
input:
weights: 重量数组
item_counts: 物品数量数组
max_weight: 最大重量
output:
背包装满方案
"""
combinations = [[] for _ in range(max_weight + 1)]
combinations[0].append([0] * len(weights))
for index, weight in enumerate(weights):
for current_weight in range(max_weight + 1):
if combinations[current_weight]:
if current_weight + weight > max_weight:
break
current_combinations = [
comb[:] for comb in combinations[current_weight]
]
new_combinations = []
for comb in current_combinations:
comb[index] += 1
if comb[index] <= item_counts[index]:
new_combinations.append(comb)
combinations[current_weight + weight].extend(new_combinations)
return combinations[-weights[0] :]
def integer_program(num_arrs, nums):
"""
input:
num_arrs: num set
nums: minimize num
output:
result
"""
num_arrays_list = []
for vals in num_arrs:
num_arrays_list.extend(vals)
nums_mat = np.array(num_arrays_list)
print("\t", nums_mat.shape)
A = nums_mat.T
b = nums
prob = lp.LpProblem("The_GY_Problem", lp.LpMinimize)
x = [
lp.LpVariable("x_%06d" % i, lowBound=0, cat="Integer")
for i in range(A.shape[1])
]
prob += lp.lpSum(x), "Total Number"
for i in range(len(b)):
prob += (
lp.lpSum([A[i][j] * x[j] for j in range(A.shape[1])]) >= b[i],
"lb%04d" % weights_array[i],
)
prob.solve()
print("\tStatus:", lp.LpStatus[prob.status])
res = []
for v in prob.variables():
if v.varValue:
res.append((A[:, int(v.name[2:])], v.varValue))
return res
# 初始化变量
weights_array = np.array(
[26, 30, 40, 56, 60, 76, 80, 86, 90, 100, 110, 116, 120, 176, 180, 200]
)
nums_array = np.array([2, 10, 8, 16, 80, 6, 32, 4, 36, 4, 10, 8, 18, 40, 16, 8])
bag_weight = 600
# 动态规划获取所有可能的背包装满方案(不能再放入任一物品)
weight_num_arrays = bag_program(weights_array, nums_array, bag_weight)
# 理论最少背包数
bags_num_min = np.ceil(sum(weights_array * nums_array) / bag_weight)
print("理论最少", bags_num_min)
# 迭代进行整数规划
# 迭代?方案数量较多,未知数多,求解慢
# 从较少方案集入手,慢慢增加方案集,直至获得最优解
for i in range(1, len(weight_num_arrays) + 1):
print(i)
# 方案数量为0,与上次迭代没变化
if not weight_num_arrays[-i]:
continue
result = integer_program(weight_num_arrays[-i:], nums_array)
bags_num = sum([n[-1] for n in result])
print("\t", bags_num)
# 当达到理论最小数量时,已获得最优解决方案
if bags_num == bags_num_min:
break
print("实际最少", sum([n for _, n in result]))
# # 承重验证
# np.all([sum(v * weights_array) <= bag_weight for v,_ in result])
# # 数量验证
# np.all(np.sum(np.array([v * n for v, n in result]), axis=0) - nums_array >= 0)
理论最少 49.0
1
(7122, 16)
Status: Optimal
50.0
2
3
(16896, 16)
Status: Optimal
49.0
实际最少 49.0
通过动态规划结合整数规划的求解方法,最终得到了49个背包的最优解,达到了理论最小值。
结论
在解决多背包问题时,贪心算法结合整数规划是一种快速有效的近似解法,适合处理规模较小的问题。而动态规划结合整数规划则能保证找到全局最优解,尽管计算复杂度较高。
根据具体的应用场景和对解的精度要求,可以选择合适的算法来进行求解。在此案例中,动态规划加整数规划的方案最终实现了最优解,并有效地减少了背包的数量。