针对 CUMCM-2025 开始学习 COPT 求解器的使用,学习如何应用 COPT 的 Python API (coptpy
) 来建模并求解数学建模中常见的离散优化问题。
Basic API
本节将介绍 COPT
求解器 Python API (coptpy)
的核心组件和常用方法。
Envr Class
Envr
类用于创建一个 COPT
环境。它是所有模型操作的起点。
Creating Environment and Model:
|
|
name
:模型的名称,此为可选参数。
Model Class Properties and Methods
Model
类是 COPT
的核心,代表了优化模型,包含了所有变量、约束和目标函数。
Basic Properties
在模型求解后,可以通过以下属性获取其基本信息:
model.status
:模型解的状态。此属性指示模型是否找到了最优解、无可行解等。例如,COPT.OPTIMAL
表示已找到最优解。model.objval
:目标函数值。此属性存储模型的最优目标函数值。
Adding Variables
可以通过 addVar()
和 addVars()
方法向模型中添加决策变量。
Adding a Single Variable:
使用 model.addVar()
方法向模型中添加一个单独的决策变量。
|
|
lb
:变量的下界(默认为 $ 0.0 $)。ub
:变量的上界(默认为COPT.INFINITY
,表示正无穷)。vtype
:变量的类型,可以是:cp.COPT.CONTINUOUS
(连续变量)cp.COPT.INTEGER
(整数变量)cp.COPT.BINARY
(二进制变量,即 $ 0 $ 或 $ 1 $)
name
:变量的名称。
Adding Multiple Variables:
使用 model.addVars()
方法一次性添加一组变量。该方法返回一个 tupledict
对象,其键为变量的下标,值为相应的 Var
对象。
|
|
*indices
:用于定义变量下标的参数。lb
/ub
/vtype
:含义与addVar
相同。obj
:变量在目标函数中的系数,默认为 $ 0.0 $。nameprefix
:变量名称的前缀。
Form 1: Using Integer Parameters to Define Dimensions:
这将创建一个具有给定维度和连续整数下标的变量组。
|
|
输出:
|
|
可以通过下标访问这些变量:
|
|
输出:
|
|
可以看到 $ x $ 实际上是一个 tupledict
对象,其键是变量下标(如 (0,0)
),值是 Var
对象。
Form 2: Using tuplelist
for Non-contiguous Indices:
当变量下标不是连续整数时,可以使用 coptpy.tuplelist
来明确指定需要创建的变量。
|
|
输出:
|
|
Form 3: Using Multiple Lists for Cartesian Product:
*indices
可以是多个列表,addVars
会自动生成这些列表的笛卡尔积作为变量的下标。
|
|
输出:
|
|
从此示例可以看出,变量的下标也可以是字符串类型。
Adding Constraints
可以通过 addConstr()
和 addConstrs()
方法向模型中添加约束。
Adding a Single Constraint:
使用 model.addConstr()
添加一个约束。
|
|
lhs
:约束的左侧表达式。对于线性约束,可以是Var
对象、LinExpr
对象或ConstrBuilder
对象。sense
:约束的类型,可以是:cp.COPT.LESS_EQUAL
(<=
)cp.COPT.EQUAL
(==
)cp.COPT.GREATER_EQUAL
(>=
)
rhs
:约束的右侧值。name
:约束的名称。
例子:
|
|
Adding Multiple Constraints:
使用 model.addConstrs()
方法添加一组类似的约束,通常通过生成器表达式来完成。
|
|
nameprefix
:约束名称的前缀,COPT 会自动在其后添加下标。
Adding Indicator Constraints:
指示约束是一种特殊类型的约束,它在一个二值变量取特定值时激活一个线性约束。
|
|
binvar
:作为指示器功能的二进制变量。binval
:binvar
的目标值(True
或False
/ $ 1 $ 或 $ 0 $),当binvar
取此值时,线性约束被激活。lhs
/sense
/rhs
:定义被激活的线性约束(与addConstr
类似)。
例子:
|
|
Setting the Objective Function
使用 model.setObjective()
方法定义模型的优化目标。
|
|
expr
:目标函数表达式,通常是一个LinExpr
对象。sense
:目标的优化方向,可以是cp.COPT.MAXIMIZE
或cp.COPT.MINIMIZE
。
例子:
|
|
Retrieving Model Information
以下方法用于获取模型求解后的各种信息。
Getting All Variables:
|
|
- 返回一个
VarArray
对象,包含模型中的所有变量。
Getting LP Analysis Results:
|
|
- 仅适用于线性规划 (LP) 模型。
- 返回一个四元组,其中每个元素都是一个列表:
values
:变量的取值。slacks
:松弛变量的取值。duals
:约束的对偶变量取值。redcosts
:变量的 Reduced Cost。
Getting Specific Model Information:
|
|
infoname
:待获取信息的名称(如COPT.Info.VarPrimal
获取变量值,COPT.Info.ConDual
获取对偶值等)。args
:指定要获取信息的变量或约束。- 若
args
为单个Var
或Constraint
对象,则返回其信息值常量。 - 若
args
为列表、VarArray
或ConstrArray
对象,则返回信息值组成的列表。 - 若
args
为字典或tupledict
对象,则返回键为指定变量/约束的下标,值为其信息值的tupledict
对象。
- 若
Cloning a Model
|
|
- 创建模型的深拷贝,
mcopy
是一个与原模型独立的新模型实例。
Var Class
Var
类表示模型中的一个单独决策变量。
Attributes
在模型求解后,可以通过以下属性获取变量的相应信息。
var.x
或var.Value
:变量的当前取值。var.name
:变量的名称。var.Slack
:松弛变量的取值(对于约束,此属性在变量层面通常不直接使用)。var.Dual
:对偶变量的取值(与约束的对偶值相关,在变量层面通常不直接使用)。var.LB
:变量的下界。var.UB
:变量的上界。
Methods
varname = v.getName()
:获取变量v
的名称。v.setName('new_name')
:设置变量v
的名称。x.remove()
:从模型中删除变量x
。
VarArray Class
VarArray
类是一个特殊的容器,用于存储一组 Var
对象。例如,model.getVars()
的返回值就是 VarArray
类型。
Methods
var = vararr.getVar(idx)
:根据变量在VarArray
对象中的下标获取相应的变量,返回一个Var
对象。varall = vararr.getAll()
:获取VarArray
对象中的所有变量,返回一个列表。arrsize = vararr.getSize()
:获取vararr
中变量的个数。
Important Data Structures
coptpy
库提供了一些特殊的数据结构,以方便处理变量和约束的集合,特别是多维情况。
multidict
multidict
函数可以将输入字典拆分为多个平行字典。当有一个字典,其值是列表或元组,并且希望将每个子元素作为单独的字典时,它非常有用。
|
|
输出:
|
|
multidict
返回一个元组,第一个元素是原始字典的所有键组成的列表,随后的元素是根据原始字典值中对应位置元素生成的字典。
tupledict
tupledict
是 Python
内置 dict
的子类,专为使用元组作为键的字典进行了优化。它继承了 dict
的所有方法,并额外提供了 select
、sum
和 prod
等方便的集合操作。model.addVars()
的返回值就是一个 tupledict
。
Creating a tupledict
:
|
|
输出:
|
|
可以使用下标访问 tupledict
中的元素,如同访问普通字典一样:
|
|
输出:
|
|
select
Method:
tupledict.select(pattern)
方法根据指定的模式筛选出符合条件的项,返回包含所有符合条件的值的列表。pattern
中的 *
代表通配符。注意,pattern
匹配的是 tupledict
的键。
|
|
输出:
|
|
sum
Method:
expr = tupledict.sum(pattern)
方法根据指定的模式筛选并累加项,返回一个 LinExpr
对象(如果 tupledict
中的值是数字)。这里的 pattern
用法与 select
方法完全相同。
|
|
输出:
|
|
prod
Method:
expr = d.prod(coeff, pattern)
方法根据指定的模式筛选,并与乘积系数累乘项,返回一个 LinExpr
对象。coeff
是一个字典或 tupledict
类型,其键需要与 d
的键对应。pattern
的含义与 select
相同。本质上,prod
方法执行的是点积操作。
|
|
输出:
|
|
tuplelist
tuplelist
是 Python
内置 list
的子类,专门用于存储元组列表。它继承了 list
的所有方法,并增强了 select
和 add
等功能。
Creating a tuplelist
:
|
|
输出:
|
|
可以直接用索引访问列表的成员。
add
Method:
tl.add((2, 3))
方法用于向 tuplelist
中添加成员,功能类似于 list.append()
。
|
|
输出:
|
|
select
Method:
select
方法的思想与 tupledict
中的 select
相同,根据指定的模式筛选符合条件的元组。
|
|
输出:
|
|
Mixed-Integer Programming (MIP)
离散优化的核心工具是混合整数规划 (Mixed-Integer Programming, MIP)。一个标准的 MIP 模型包含三个核心要素:
决策变量 (Decision Variables):模型中需要求解的未知数。在离散优化中,这些变量通常被约束为整数(例如,工人数、产品件数)或 0-1 二进制变量(例如,某个决策是否执行)。
目标函数 (Objective Function):一个关于决策变量的线性表达式,我们需要将其最大化(如利润、效率)或最小化(如成本、距离)。
约束条件 (Constraints):一系列关于决策变量的线性等式或不等式,用于定义问题的可行域。
我们的任务就是将一个具体问题用这三个要素清晰地表达出来,然后交给 COPT 进行求解。
0-1 Knapsack Problem
对于 0-1 背包问题,定义 $ N $ 为物品总数,$ w_{i},v_{i} $ 分别为物品 $ i $ 的重量和价值,$ C $ 为背包的容量。那么就可以列出这个问题的三个要素:
- 决策变量:定义二进制串 $ x_{i} $ 表示是否选取。 $$ x_i \in \{0, 1\}, \quad \forall i \in \{1, ..., N\} $$
- 目标函数:最大化背包内物品的总价值。 $$ \max \sum_{i=1}^{N} v_i x_i $$
- 约束条件:装入背包的物品总重量不能超过背包容量。 $$ \text{s.t.} \quad \sum_{i=1}^{N} w_i x_i \leq C $$ 那么就可以得到 COPT 的求解代码:
|
|
Traveling Salesperson Problem (TSP)
旅行商问题。给定一系列城市和每对城市之间的距离,求解访问每一座城市一次并最终回到起始城市的最短可能路径。此问题的一个经典 MIP 模型是 Miller-Tucker-Zemlin (MTZ) 公式。
定义 $ N $ 为城市总数量,$ d_{i,j} $ 为城市 $ i $ 到城市 $ j $ 的距离,那么可以得到:
- 决策变量:
- 二进制变量 $ x_{i,j} $ ,表示是否经过 $ i $ 到 $ j $ 之间路径。 $$ x_{ij} \in \{0, 1\}, \quad \forall i, j \in \{0, ..., N-1\}, i \neq j $$
- 辅助连续变量 $ u_{i} $ 用于消除子回路 $$ u_i \ge 0, \quad \forall i \in \{0, ..., N-1\} $$
- 目标函数:最小化总路程。 $$ \min \sum_{i=0}^{N-1} \sum_{j=0, j\neq i}^{N-1} d_{i,j} x_{i,j} $$
- 约束条件:
- 每个城市必须且只能进入一次。 $$ \sum_{i=0, i\neq j}^{N-1} x_{i,j} = 1, \quad \forall j \in \{0, ..., N-1\} $$
- 每个城市必须且只能离开一次。 $$ \sum_{j=0, j\neq i}^{N-1} x_{ij} = 1, \quad \forall i \in \{0, ..., N-1\} $$
- 消除子回路约束。 $$ u_i - u_j + N \cdot x_{ij} \le N-1, \quad \forall i, j \in \{1, ..., N-1\}, i \neq j $$ 那么就可以得到代码:
|
|
其余问题方法均类似,在此不再赘述,总之解决问题的流程就是定义决策变量,构建目标函数,构建约束三步。
Advanced Tips
在比赛中需要高效的建模和调试。
Debugging Infeasible Models
当模型没有可行解时 (model.status == COPT.INFEASIBLE
),意味着约束条件之间存在冲突。找出问题所在非常困难。COPT 提供了不可约不一致子系统 (Irreducible Inconsistent Subsystem, IIS) 工具来定位问题。IIS 是原始模型约束的一个最小子集,其本身是不可行的。
使用流程:
- 首先调用
model.solve()
并确认模型状态为不可行。 - 调用
model.computeIIS()
来计算 IIS。 - 遍历模型的约束,通过检查其
IISConstr
属性,找出属于 IIS 的约束。这些约束就是导致模型不可行的元凶。
|
|
Parameter Tuning for Performance
对于大规模或复杂的 MIP 问题,默认参数可能无法在有限时间内找到好的解。可以使用 COPT 的参数调优工具 (Tuner) 自动寻找最佳参数组合。
在 Python 中,可以通过 model.tune()
方法来启动调优器。它会尝试不同的参数设置,并在调优过程结束后,将最优参数应用到模型上。
|
|