如何写一个CPLEX 应用程序(C++)
根据CPLEX用户手册,介绍以 C++ 编写的传统 CPLEX 应用程序的体系结构。
总体流程
大部分应用程序有可能需要的步骤如下所示。
- 首先,使用 Concert Technology 的建模工具来创建问题模型。 使用 Concert Technology 对优化问题进行建模提供有关创建模型的简介。
- 模型准备好进行求解后,请将其交给 CPLEX 进行求解。 对模型求解说明如何执行此操作。 它对用于控制优化的 IloCplex 接口作了阐述。 在说明各个优化器的章节中,对各个控件作了探讨。
- 在 CPLEX 求解模型之后,存取解法信息说明如何访问和解释优化结果。
- 分析结果后,您可能希望对模型进行更改并研究其效果。 修改模型说明如何进行更改以及 CPLEX 处理更改的方式。
- 处理错误探讨 Concert Technology 和 CPLEX 所提供的错误处理和调试支持。
- 示例:在 C++ 中优化饮食方案问题呈现完整的程序。
建模
创建环境:IloEnv
在创建建模对象之前,必须构造 IloEnv
类的对象。 此对象称为环境。 可使用以下语句来构造此对象:
IloEnv env;
此语句通常是应用程序中的第一条 Concert Technology 语句(int main之后)。 在结束时,必须通过调用以下方法关闭环境(try{}之外):
env.end();
此语句通常是应用程序中的最后一条 Concert Technology 语句。 必须调用 end
方法,因为就像大部分 Concert Technology 类一样,IloEnv
类是句柄类。 即,IloEnv
对象实际上只是指向实现对象的指针。 实现对象可通过调用 end
方法予以销毁。 不调用 end
方法可能会导致内存泄漏。
定义变量和表达式:IloNumVar
需要的第一个建模类可能是 IloNumVar
。 这个类的对象表示模型中的决策变量。 它们由变量的下限和上限定义,类型可以是 ILOFLOAT
、ILOINT
或 ILOBOOL
,分别代表连续变量、整数变量或布尔型变量。 以下构造函数将创建界限为 -1 和 10 的整数变量:
IloNumVar myIntVar(env, -1, 10, ILOINT);
类 IloNumVar
提供了用于查询指定变量时所需数据的方法。 但是,只有界限可修改。 Concert Technology 提供了建模对象类 IloConversion
,用于更改变量的类型。 此转换允许在不同模型中使用具有不同类型的同一变量。
变量通常用来构建表达式,后者进而用来定义优化问题的目标或约束。 表达式可以显式地编写,例如:
1*x[1] + 2*x[2] + 3*x[3]
其中,假定 x
是变量数组 (IloNumVarArray
)。 另外,还可以使用循环来逐段创建表达式:
IloExpr expr(env);
for (int i = 0; i < x.getSize(); ++i)
expr += data[i] * x[i];
完成使用表达式之后(即,使用表达式来创建约束之后),需要通过调用其 end
方法将其删除,例如:
expr.end();
请尽可能依据整数数据或双精度(64 位)浮点数据来构建表达式。 应该避免使用单精度(32 位)浮点数据,因为这可能会不必要地生成情况不良的问题。 有关更多信息,请参阅数字难度。
虽然 Concert Technology 支持非常一般的表达式,但在要使用 IloCplex
来求解的模型中,只能使用线性表达式、二次表达式、分段线性表达式和逻辑表达式。具体查看IBM的资料。
声明目标:IloObjective
类 IloObjective
的对象表示优化模型中的目标函数。 IloCplex 只能处理最多有一个目标函数的模型,但是 Concert Technology 所提供的建模 API 未实施此限制。要指定目标函数,请创建 IloObjective
的实例。 例如:
IloObjective obj(env, 1*x[1] + 2*x[2] + 3*x[3], IloObjective::Minimize);
定义将表达式 1*x[1] + 2*x[2] + 3*x[3]
最小化的目标。
添加约束:IloConstraint 和 IloRange
类 IloConstraint
的对象表示模型中的约束。 大部分约束将属于从 IloConstraint
派生的子类 IloRange
,因此将继承其构造函数和方法。 IloRange
表示以下格式的约束:lower bound
≤
expression
≤
upper bound
。换言之,IloRange
的实例是一种表示范围约束(即,具有明确上下限的约束)的便捷方法。 可以使用任何浮点值或者 +IloInfinity
或 -IloInfinity
来表示界限。 例如:
IloRange r1(env, 3.0, x[1] + x[2], 3.0);
定义约束 x[1] + x[2] == 3.0
。
阐述问题:IloModel
要阐述完整的优化问题,您需要创建属于该问题的对象并将这些对象添加到 IloModel
(表示优化问题的类)的实例、。 例如,下列行:
IloModel model(env);//创建模型
model.add(obj);//将目标函数添加至模型
model.add(r1);//将约束添加至模型
将定义一个模型,该模型由目标 obj
、约束 r1
及其所使用的所有变量组成。 请注意,无需将变量显式地添加到模型,因为模型中的任何其他建模对象使用这些变量时,即会隐式地考虑这些变量。 (但是,可将变量显式地添加到模型,例如,如果将某个变量视为问题的组成部分,那么即使它未出现在任何约束或目标函数中,也可进行添加。)
为方便起见,Concert Technology 提供了函数 IloMinimize
和 IloMaximize
,用于定义最小化和最大化目标函数。 并且,对运算符 <=、==
和 >=
进行了重载,以创建 IloRange
约束。 这些功能使您能够紧凑易读地重新编写示例,如下所示:
IloModel model(env);
model.add(IloMinimize(env, 1*x[1] + 2*x[2] + 3*x[3]);
model.add(x[1] + x[2] == 3.0);
使用这种表示法时,不需要像此示例原先那样显式创建 C++ 变量 obj
和 r1
。 相反,这些变量通过第二个示例中的变量来表示。
类 IloModel
本身是建模对象的类。 因此,一个模型可以添加到另一个模型。 此功能的一种可能用法是,在本身作为核心模型的扩展的不同模型中捕获不同的方案。 核心模型可以表示为 IloModel
对象本身,并添加到表示各个方案的 IloModel
对象。
管理数据
通常,在创建模型的 Concert Technology 表示之前或创建期间,必须收集优化问题的数据。 但是,在原则上,建模并不依赖于生成和表示数据的方式,此任务可以由 Concert Technology 所提供的数组类或集合类(例如 IloNumSet
)协助完成。
例如,可以使用类 IloNumArray
的对象在数组中存储数字数据。 可以像访问标准 C++ 数组的元素一样访问类 IloNumArray
的元素,但是这个类还提供了许多其他功能。 例如,Concert Technology 数组可扩展;换言之,使用 add
方法添加新元素时,这些数组可以透明地修改为所需大小。 反过来,可使用 remove
方法从数组中的任何位置移除元素。 以调试方式编译后,Concert Technology 数组还会提供调试支持,即,可使用 assert
语句来确保不会访问数组边界以外的元素。 针对数组,提供了输入和输出运算符(即,operator <<
和 operator >>
)。 例如,以下代码:
IloNumArray data(env, 3, 1.0, 2.0, 3.0);
cout << data << endl;
将生成以下输出:
[1.0, 2.0, 3.0]
完成使用数组并希望回收其内存时,请调用 end
方法;例如,data.end()
。 环境结束时,属于同一环境的数组的所有内存也将返回给系统。 因此,在实践中,不需要正好在调用 env.end
之前对数组或任何其他 Concert Technology 对象调用 end
。
数组的构造函数指定构造大小为 3 的数组,其中包含元素 1.0、2.0 和 3.0。 例如,可使用以下代码读回此输出格式:
cin >> data;
最后,Concert Technology 提供了模板类 IloArray<X>
,用于为您自己的类型 X 创建数组类。此技术可用于生成多维数组。 除输入/输出以外的 IloArray
类支持这里提到的所有函数,而输入/输出依赖于针对类型 X 定义的输入和输出运算符。
求解模型
抽取模型
这里仅定义一种优化模型,并且一次仅使用 IloCplex 的一个实例来求解模型。
但是,在 Concert Technology 中,可创建任意数目的模型和算法对象。 cplex
对象可以由构造函数创建:
IloCplex cplex(env);
要使用 CPLEX 来求解模型,必须先使用类似于以下的调用,将该模型抽取到 cplex
中:
cplex.extract(model);
此方法将数据从模型复制到相应的高效数据结构中,CPLEX 将使用这些数据结构来求解问题。 它通过抽取每个已添加到模型的建模对象以及它们所引用的每个对象来完成此操作。 对于所抽取的每个建模对象,将以内部方式在 cplex
对象中创建相应数据结构。 对于熟悉 CPLEX 内部所使用的稀疏矩阵表示法的读者而言,变量变为列,约束变为行。 如稍后所述,这些数据结构与建模对象同步,即使对建模对象进行了修改也是如此。
如果将某个变量视为模型的组成部分,那么即使(最初)未在任何约束中使用此变量,也应该将此变量显式地添加到模型中。 这样做可确保抽取此变量。 这样做对于查询变量的解信息来说也可能十分重要,其原因在于,只有 CPLEX 已知的建模对象才有可用的解信息(因为已从模型中抽取这些对象)。
当不确定是否会抽取某个对象时,可将其添加到模型以加以确定。 对象即使添加多次也只会抽取一次,因此不会拖慢求解过程。
由于创建 cplex
对象并将模型抽取到对象的序列很常用,IloCplex 提供了快捷方式(即使用以下语句取代上述两句语句):
IloCplex cplex(model);
调用求解器
将模型抽取到 cplex
对象之后,即可通过调用以下方法对其进行求解:
solve();
对于大部分问题,这就是求解模型所需的所有操作。 但是,CPLEX 提供了各种控件,它们允许定制求解过程,以满足特定需求。
选择优化器
使用 CPLEX 来求解所抽取的模型涉及到求解一个或一系列连续松弛:
-
如果所抽取的模型本身是连续的,即,如果它未包含整数变量、布尔型变量、半连续或半整数变量、逻辑约束、特殊有序集 (SOS) 或分段线性函数,那么只需要求解一个连续松弛。 求解 LP:单纯形法优化器和求解 LP:barrier 优化器探讨可用于求解 LP 的算法。 同样,求解具有二次目标 (QP) 的问题探讨可用于求解 QP 的算法。 使用二次约束 (QCP) 对问题求解在二次约束规划问题 (QCP) 的上下文中重新介绍 barrier 优化器。 在优化中使用分段线性函数:运输示例通过一个运输示例来介绍分段线性函数。 优化中的逻辑约束介绍逻辑约束,随后的章节提供示例。
-
在所有其他情况下,CPLEX 看到的所抽取问题实际上是 MIP,一般而言,这是需要求解的一_系列_连续松弛。 在此类情况下,方法
cplex.isMIP
将返回IloTrue
。 求解混合整数规划 (MIP) 问题探讨所应用的算法。
用于求解第一个连续松弛(无论这是唯一的问题还是一系列问题中的第一个问题)的优化器选项由根算法参数控制:
cplex.setParam(IloCplex::RootAlg, alg);
具体的算法查IBM的参数表。
控制优化器
存取解法信息
访问解状态
调用 solve 将返回一个布尔值,该值指定是否已找到可行解(但不一定是最优解)。要获取有关 CPLEX 在调用 solve
方法期间找到的模型的更多信息,请调用 getStatus 方法。 它返回嵌套枚举 IloAlgorithm_Status 的成员。 这些符号的标准名称具有 IloAlgorithm
前缀。 表 表 1 列出对于所抽取的模型,每种返回状态的含义。关于模型的算法状态和信息
查询解数据
如果 getValue 返回 IloTrue
,那么说明已找到可行解,并且模型变量的解值可供查询。例如,可以采用类似如下的方式来访问数字变量 var1
的解值:
IloNum x1 = getValue(var1
);
但是,逐个变量地查询解值可能会导致代码臃肿。 在这里,使用 Concert Technology 数组可以更加紧凑的方式来访问解值。 假定变量存储在名为 var
的数字变量数组 (IloNumVarArray)
中,那么使用类似于以下的行可同时访问 var
中所有变量的解值:
IloNumArray x(env); getValues(x, var
);
值 x[i]
包含变量 var[i]
的解值。
解数据并不限于变量的解值。 它还包括约束(无论是线性约束还是二次约束)中松弛变量的值以及目标值。 如果抽取的模型未包含目标对象,那么 IloCplex
将采用 0 表达式目标。 目标值由调用 getObjValue 方法返回。 要访问松弛值,请使用 getSlack 和 getSlacks 方法,这两个方法接收线性约束、二次约束或约束数组作为参数。