前言
在之前发布的智能解决装箱问题:使用优化算法实现高效包装一文中,我们已经介绍了什么是组合优化问题。这里仅简述:
组合优化是数学优化的一支,专注于从有限集合中选取元素的最优化问题。它涉及将一组对象组合在一起,以满足特定条件并优化某个目标函数,即在所有可能的组合中找到最有利的一个。
本文将以一个简化的背包问题为例,来讲解采用数学规划的方法来解决背包这个组合优化问题。
背包问题
背包问题(Knapsack Problem)和装箱问题(Packing Problem)是两类经典的组合优化问题。虽然在概念上有所相似,都涉及物品的选择和分配,它们的优化目标和具体应用场景却有所不同。
背包问题
目标是在不超过背包容量限制的条件下,选取总价值最大化的物品组合。应用场景广泛,如行李打包、物流、购物选择、广告投放、投资组合、资源分配。
装箱问题
目标是在尽量减少容器数量或最小化容器空余容量的前提下将物品装入箱子。常见的应用场景包括集装箱装载、仓库货物分配、内存分配等。
最明显的区别,是优化目标:
- 背包问题:最大化所装物品的价值
- 装箱问题:最小化容器数量
问题描述以及数学规划建模
假设小美要出远门长住几个月,她带了4个包,需要带更有价值的物品。有一堆物品可以带,如果忽略物品刚性形状,我们假设所有物品都可以变形,评估物品体积、重量和有用价值如表格 data/items-1.csv
,部分内容如下:
物品ID | 物品重量(g) | 物品体积(cm^3) | 价值因子 |
---|---|---|---|
1 | 161 | 1807 | 14 |
2 | 794 | 4109 | 55 |
3 | 255 | 4952 | -4 |
4 | 841 | 5773 | 88 |
… | … | … | … |
100 | 753 | 1231 | 49 |
物品和背包属性如下:
- 物品集合
I
I
I
- 它的描述信息参数是 重量 w i w_{i} wi、尺寸 s i s_{i} si 和 价值 v i v_{i} vi
- 假设有背包集合
B
B
B
- 它的描述信息参数是 承重上限 w b , m a x w_{b,max} wb,max 、 尺寸上限 s b , m a x s_{b,max} sb,max
变量:
- 货物如何摆放,设置 0-1 变量 x i , b x_{i,b} xi,b,其中 i ∈ I i \in I i∈I, b ∈ B b \in B b∈B。 当变量取值为1的时候,则代表物品装进该背包,为0的时候则不放置。
建模目标和约束条件:
目标函数: max ∑ i ∈ I , b ∈ B x i , b ⋅ v i \max \sum_{i \in I, b \in B} x_{i,b} \cdot v_{i} max∑i∈I,b∈Bxi,b⋅vi
约束条件:
- 每个货物最多只能放一个位置: ∑ b ∈ B x i , b ≤ 1 , ∀ i ∈ I \sum_{b \in B} x_{i,b} \leq 1, \forall i \in I ∑b∈Bxi,b≤1,∀i∈I
- 每个货车上,装载的货物重量和体积限制:
- ∑ i ∈ I x i , b ⋅ w i ≤ w b , m a x , ∀ b ∈ B \sum_{i \in I} x_{i,b} \cdot w_{i} \leq w_{b,max}, \forall {b \in B} ∑i∈Ixi,b⋅wi≤wb,max,∀b∈B
- ∑ i ∈ I x i , b ⋅ s i ≤ s b , m a x , ∀ b ∈ B \sum_{i \in I} x_{i,b} \cdot s_{i} \leq s_{b,max}, \forall {b \in B} ∑i∈Ixi,b⋅si≤sb,max,∀b∈B
MindOpt APL 编程和求解
MindOpt APL是达摩院研发的代数建模语言,国内首款!语法简单!点击了解>>
使用MindOpt APL进行建模和求解。导入背包和物品的数据,定义变量、目标函数和约束,调用求解器获取最优解。
clear model;
option modelname knapsackproblem_01; #定义存储文件名
# ----------建模--------Start----
# knapsackproblem_01.mapl
## 导入数据-------
print "导入数据-------";
param fileDir = "data/";
# 物品数据
param ItemsFilename = "items-1.csv"; # 有10份数据,名字分别是 items-1.csv,items-2.csv……
set Items = {read fileDir+ItemsFilename as "<1n>" skip 1}; #读取货物序号
param ItemWeight[Items] = read fileDir+ItemsFilename as "<1n> 2n" skip 1; #读取物品的重量,单位克g
param ItemSize[Items] = read fileDir+ItemsFilename as "<1n> 3n" skip 1; #读取物品的尺寸,单位立方厘米cm^3
param ItemValue[Items] = read fileDir+ItemsFilename as "<1n> 4n" skip 1; #读取物品的价值,有正有负
print "数据:{}"%ItemsFilename;
print "- 总共有{}个可选择物品,总重量{}g,总体积{}cm^3,总价值{},其中正价值总和{},负价值总和{}"
% card(Items),
sum{i in Items}ItemWeight[i],
sum{i in Items}ItemSize[i],
sum{i in Items}ItemValue[i],
sum{i in Items with ItemValue[i] >0 } ItemValue[i],
sum{i in Items with ItemValue[i] <=0 } ItemValue[i];
# 背包数据
param BagsFilename = "bags.csv";
set Bags = {read fileDir+BagsFilename as "<1n>" skip 1}; #读取货物序号
param BagWeightCapacity[Bags] = read fileDir+BagsFilename as "<1n> 2n" skip 1; #读取包的重量承重,单位克g
param BagSizeCapacity[Bags] = read fileDir+BagsFilename as "<1n> 3n" skip 1; #读取包的体积容量,单位立方厘米cm^3
print "- 总共有{}个可选择物品,承重上限{}g,容积上限{}cm^3"
% card(Bags),
sum{b in Bags} BagWeightCapacity[b],
sum{b in Bags} BagSizeCapacity[b];
## 数学建模-------
print "数学建模--------------";
# 变量----
var x_Item2Bag[Items*Bags] >= 0 binary; #物品放哪个包
# 目标----
# 目标函数:最大化所带物品的价值
maximize TotalValue: sum { <i,b> in Items*Bags } x_Item2Bag[i,b] * ItemValue[i];
# 每个物品最多只能放一个位置,且至少是一个位置
subto constraint_0:
forall {i in Items}
sum {b in Bags} x_Item2Bag[i,b] <= 1;
# 每个包的容量限制
# 重量
subto constraint_1:
forall {b in Bags}
sum { i in Items} x_Item2Bag[i,b] * ItemWeight[i] <= BagWeightCapacity[b];
# 体积
subto constraint_2:
forall {b in Bags}
sum { i in Items} x_Item2Bag[i,b] * ItemSize[i] <= BagSizeCapacity[b];
#求解
print "调用求解器求解--------------";
option solver mindopt; # (可选)指定求解用的求解器,默认是MindOpt
#option mindopt_options 'max_time=600'; #设置求解器参数
#option mindopt_options 'print=0'; #设置求解器输出级别,减少过程打印
solve; # 求解
# 结果打印和检查结果
print "结果打印和存文件--------------";
#display;
print "- 带的物品数 = ",sum { <i,b> in Items*Bags } x_Item2Bag[i,b];
print "- 总价值是 = ",sum { <i,b> in Items*Bags } x_Item2Bag[i,b] * ItemValue[i];
print "- 包里面摆放物品清单:";
print "|背包ID|物品ID|体积|重量|价值|";
print "|--|--|--|--|--|";
# 由于打印太多,只打印小报包的,如需打印全部,可以删除`with b>2 `
forall { b in Bags with b>2 }
forall { i in Items with x_Item2Bag[i,b]>= 0.5}
print "|{}|{}|{}|{}|{}|"%b,i,ItemWeight[i],ItemSize[i],ItemValue[i];
print "|……|……|……|……|……|";
# 存文件
print "背包ID,物品ID,体积,重量,价值": "每个背包的物品清单.csv";
close "每个背包的物品清单.csv";
forall { b in Bags }
forall { i in Items with x_Item2Bag[i,b] >= 0.5}
print "{},{},{},{},{}"%b,i,ItemWeight[i],ItemSize[i],ItemValue[i] >> "每个背包的物品清单.csv";
close "每个背包的物品清单.csv";
print "- 结果文件存储在: [每个背包的物品清单.csv](每个背包的物品清单.csv)";
运行上述代码结果如下:
导入数据-------
数据:items-1.csv
- 总共有100个可选择物品,总重量44795g,总体积326428cm^3,总价值4342,其中正价值总和4411,负价值总和-69
- 总共有4个可选择物品,承重上限34000g,容积上限237000cm^3
数学建模--------------
调用求解器求解--------------
Running mindoptampl
wantsol=1
MindOpt Version 1.1.1 (Build date: 20240306)
Copyright (c) 2020-2024 Alibaba Cloud.
Start license validation (current time : 27-MAR-2024 16:13:18).
License validation terminated. Time : 0.008s
Model summary.
- Num. variables : 400
- Num. constraints : 108
- Num. nonzeros : 1200
- Num. integer vars. : 400
- Bound range : [1.0e+00,1.2e+05]
- Objective range : [1.0e+00,9.9e+01]
Branch-and-cut method started.
Original model: nrow = 108 ncol = 400 nnz = 1200
Tolerance: primal = 1e-06 int = 1e-06 mipgap = 0.0001 mipgapAbs = 1e-06
Limit: time = 1.79769313486232e+308 node = -1 stalling = -1 solution = -1
presolver terminated; took 0 ms
presolver terminated; took 9 ms
Parallelism: root=2, tree=2
accept new sol: obj 0 bnd vio 0 int vio 0 mipgap inf time 0
accept new sol: obj -4201 bnd vio 0 int vio 0 mipgap 2.57295881932873 time 0
Model summary.
- Num. variables : 294
- Num. constraints : 94
- Num. nonzeros : 882
- Bound range : [1.0e+00,1.2e+05]
- Objective range : [1.0e+00,9.9e+01]
- Matrix range : [1.0e+00,6.0e+03]
Presolver started.
Presolver terminated. Time : 0.000s
Simplex method started.
Model fingerprint: mdXZv92dj5WZ3Nmb
Iteration Objective Dual Inf. Primal Inf. Time
0 -1.50100e+04 0.0000e+00 3.9240e+02 0.00s
137 -4.30372e+03 0.0000e+00 0.0000e+00 0.00s
Postsolver started.
Simplex method terminated. Time : 0.001s
accept new sol: obj -4211 bnd vio 0 int vio 0 mipgap 0.0220180858248353 time 0
accept new sol: obj -4290 bnd vio 0 int vio 0 mipgap 0.00319770615579984 time 0
Root relaxation: -4303.71815940838 iteration = 137 time = 0.002
#node(P:0 Q:0) #(dual:-4303.72 best:-4290 gap:0.32%) #time = 0
accept new sol: obj -4299 bnd vio 0 int vio 2.81374923361e-15 mipgap 0.00109750160697402 time 0
accept new sol: obj -4303 bnd vio 6.66133814775094e-16 int vio 0 mipgap 0.000166897375872955 time 1
- Solution pool : 5
Branch-and-cut method terminated. Time : 1.913s
OPTIMAL; objective 4303.00
Completed.
结果打印和存文件--------------
- 带的物品数 = 75
- 总价值是 = 4303
- 包里面摆放物品清单:
|背包ID|物品ID|体积|重量|价值|
|--|--|--|--|--|
|3|13|650|4889|67|
|3|28|684|5395|36|
|3|38|236|1966|62|
|3|47|379|3467|24|
|3|48|317|3168|94|
|3|74|402|5083|58|
|4|73|250|35|54|
|4|86|50|2411|67|
|4|95|536|510|13|
|……|……|……|……|……|
- 结果文件存储在: [每个背包的物品清单.csv](每个背包的物品清单.csv)
高阶方案尝试
此案例发布在云上建模平台中读者可尝试复制本项目,增加难度进行调试。比如:
- 在平台中的data文件夹中,我们提供了不同的item.csv数据,如
data/items-2.csv
,观察不同数据的结果区别。 - 增加物品的其他信息,如类别,标记每个类别里面至少带的数量。
- 将此模型推广到其他场景,比如:
- 在线广告:选择最优化的广告投放组合,以最大化点击率或转化率。
- 股票投资:如何在有限的资金下,挑选出能带来最大收益的股票组合。
- 资源分配:如何在有限的资源下,最大化利益。
- 云计算:在有限的资源下,为各个任务分配最优的资源组合。
- 学术研究:在有限的资源和时间下,选择最有价值的研究课题。