数模 | CPLEX求解指派/背包/选址问题,代码编写就是这么丝滑

数模 | CPLEX求解指派/背包/选址问题,代码编写就是这么丝滑

前沿

​上一期阐述了用 Matlab调用linprog等函数 求解线性/整数规划 的编程思想和代码。

优点 是转化为矩阵,求解较快; 缺点 是不能很好地处理 含有多维变量或参数 的模型。(例如intlinprog函数只能处理一维数据)

而数模比赛时,构建的模型通常较为 复杂 (参数/变量繁多且均是多维)。因此,像线性/整数规划这种 专业的问题 ,有必要交给 专业的软件

不只是 CPLEX ,还有 Gurobi/AMPL 等,它们努力做到 让你专注于写模型 ,而无需在 模型转代码 上花费太多时间。毕竟它们是 商业软件 简单好用才是王道 。总的来说,它们主要解决优化问题编程时的如下 痛点

模型怎么写的,对照着编程就好了!(别费劲地要求我们去整理成比如矩阵等形式);
很多模型长得差不多,主要是数据在变。(那编程时能否也分开,模型是模型,数据是数据,从而实现改数据时不用改模型);
能否用简单的标记就能区分小数、整数、正负变量或参数。(标记好后让软件自己去区分,无需人为)

(以上三点结合上一期 Matlab调用linprog等函数 内容,会有更深体会)

听上去是不是感觉挺 美好 的?恩,但除了美好,我们还希望 简单 !那如何才能实现呢?不着急,一起看个 例子 ,懂了它,就能掌握这类优化软件的 编程思想 啦。

一、CPLEX编程思想

仍以 之前 提到的 指派问题 为例:

指派问题:
分配n人去做n项工作;每人做且只做一项工作;若分配第i人去做第j项工作,需花费Cij成本。
问应如何分配工作使总成本最小?

建立的数学模型为:

仔细观察上述模型,有如下 特点

参数有n、cij,均为整型;集合有[1..n];变量有xij,为0-1布尔型;
定义模型时的符号有:最小化问题(minmize)、约束(subject to);
定义约束时的符号有:"求和号"、"任意号";

找到了这些 关键元素 ,接下来就是 采用一定的逻辑 把它们串接起来~

我们知道, 参数和集合都是已知量 ;而 决策变量xij是未知量 。那肯定是先做好准备工作: 写好已知量,后再去写模型 。因此,CPLEX的编程步骤为:

1. 定义参数、集合、变量

首先盯着上面指派问题中的 目标函数 看,里面有 参数cij和n ;有 集合i,j∈1..n ;有 决策变量xij

不同软件编程语法的差异在于如何定义这些量。 如CPLEX用 dvar定义变量 ,而AMPL用var定义(下一期讲)。本期专注于CPLEX的语法。

(1) 定义参数——int/float
语法是: "int n=5”或者“int n=..."; 如果数据类型是连续型,int改为float,即可以是小数。

这里允许n先不定义它是多少,用 三个点 表示。这种写法实现的是 模型文件和数据文件分开写。

这样,模型文件中不用写数据,专注于写模型即可,而所有数据放在另一个文件中写。

当然, 模型文件中同时也包含所有数据 ,不再去编写数据文件,CPLEX也是支持的。

(2) 定义集合——range
语法是:"range Worker = 1..n"

这里 Worker 是我随意起的名字; 1..n 中的 两个点 也是CPLEX语法,指 从1到n ,不可用1个点或3个点。

定义好了集合,模型中再遇到 i∈1..n 时,就可以简写成 i in Worker 了;也许有人会说,那直观写成 i in 1..n 也很简单呐,没必要再去定义个Worker吧。

道理确实是这样的,也确实 有时不去定义它 ,语法都是允许的,看大家的编程喜好了。

(3) 定义变量——dvar
语法是:"dvar 数据类型 变量";如"dvar boolean x[Worker][Job]"

dvar 的含义是 decision variable (决策变量); boolean 布尔型 ,0-1变量的意思,含义是 当第i个Worker去做第j项Job时为1,否则为0

注意, x[Worker][Job] 也可写成 x[1..n][1..n] ,但 不能写成x[i][j] 。因为定义变量时,没有下标什么事,只有写模型时才会有下标的事。

此外, 数据类型 除了 boolean ,还有整型( int ),正整数( int+ )、连续型( float )等。如定义一个 正整数x 则为: "dvar int+ x"

2. 写模型——定义目标和约束条件

语法是:"minimize 目标函数"、"subject to{约束;}"

CPLEX等软件编程能实现 "模型怎么写,程序怎么编" ,非常直观。下一节我会逐行展示。

这里多说几句。我们建模时,通常习惯使用" 集合语言" 。比如 求和号Σ ,在CPLEX中用 "sum" 表示;对 "任意的i∈1..n" ,用 "forall(i in 1..n)" 表示。

以上就是CPLEX(其他软件也适用)的 编程思想

先定义已知量,再定义未知量,然后采用"集合语言"写目标和约束。

我编了个简单的口诀帮大家记忆: 参集变,mst。

参集变:
先写已知量"参数"和"集合", 再写未知量"决策变量"
mst:
"m"指最大/小化问题,即maximize、minimize; "st"为subject to{}的简写。

下面,针对 指派/背包/选址问题 ,按照" 参集变,mst "的编程步骤,实践下吧。

二、CPLEX求解指派问题

本节采用 模型文件和数据文件分开写 的模式,下一节 背包问题展现放在一起写 的模式。(对于任意模型,两种模式皆可的)

1. 定义参数、集合、变量

已知量 参数 有n和cij; 集合 为1..n。都先不赋值,在数据文件中再给它们赋值。

int n=...; // 参数n
range Worker=1..n; // 工人集合
range Job=1..n; // 任务集合
int c[Worker][Job]=...; // 成本参数

未知量 决策变量 为xij。

dvar boolean x[Worker][Job];  // 决策变量

上述" c[Worker][Job] "即 第i个Worker去做第j项Job 。如果大家还不习惯这种 借用字符串 的写法,也可以像下面这样写:

int n=...; // 参数n
int c[1..n][1..n]=...; // 成本参数
dvar boolean x[1..n][1..n];//决策变量

相比较, 少了定义集合的两行代码 。不定义集合的话,就需一直写着" 1..n "。

不管哪种,都比较简洁直观,即使没学过这些语法,也能懂个大概。两种方式看个人偏好啦。

2. 写模型——定义目标和约束条件

准备工作做好了,那就对照着模型, 逐行 翻译成CPLEX语言吧。

目标函数语法: minimize/maximize 目标函数

回顾 指派问题的目标函数 ,有两种编码方式:

minimize sum(i in Worker)sum(j in Job) c[i][j]*x[i][j];
minimize sum(i in 1..n)sum(j in 1..n) c[i][j]*x[i][j];

两种皆可,如果定义好了 集合 ,就用集合写;如果不习惯,用" 1..n "写也非常直观。

这里需要注意"()"和"[]"的使用规范 。" sum() "相当于函数,要用" () ";参数/变量 下标调用的时候是用"[]" ;写完结尾要用 ";"

约束条件语法: subject to{约束;}

回顾指派问题的约束条件:

subject to
{ forall(i in 1..n)
    sum(j in 1..n) x[i][j] == 1;//约束1
  forall(j in 1..n) 
    sum(i in 1..n) x[i][j] == 1;//约束2
}

以约束1为例,第2行" forall(i in 1..n) "对应图片中右半部分;第3行 对j求和后等于1 就是约束1。不需要转化成矩阵啥的,语法很人性化。

依然需要注意"()"和"[]"的使用规范 。" forall() "相当于函数,要用" () ",结尾记得加 ";" 。好了,模型就这么愉快地 翻译 完啦,再新建个 数据文件存储数据 吧。

新建数据文件: Assignment.dat

之前模型编码存放于" Assignment.mod "文件中,新建后缀为 .dat 的文件存储数据:

n=3; // 3个工人去完成3个任务
c=[[3,8,2],[8,4,6],[6,2,10]]; // 成本矩阵

两行代码搞定, 注意这里的矩阵写法 。该写法类似Python,数据结构为数组; 两个数字之间的","不可省略 奥。

3. 运行CPLEX并求解

CPLEX的安装教程放在文末。安装成功后,打开CPLEX,关闭欢迎界面,选择" 文件 "--" 新建 "-- "OPL项目 "。

起个名字,如" Assignment ",并 勾选上 图中的三个选项。

这里要注意: 项目名称和位置必须是英文, 不能出现中文,否则无法运行

在新建的 "Assignment.mod"文件 中输入模型代码,在 "Assignment.dat" 文件中输入数据代码。

注意,在运行程序前, 还有个地方要改

新建完两个文件后,项目栏默认会写" 配置1(缺省值) "。虽然是默认的,但出现中文是不行的,所以要 改成英文 ,随便起个 英文名 即可。

然后,在改完名后的" Assign(缺省值) "上右键,点击" 运行这个 ",没有报错的话,就能在下方看到" 解决方案 "。

可见,目标值是11,最优解x的值也已给出。点开" 统计信息 "还有 计算时间 等数值。

以上就是 CPLEX求解指派问题 的步骤。记住" 参集变,mst "这句口诀,写代码就像吃德芙一般丝滑。

三、CPLEX求解背包问题

本节展示 模型和数据写在一起 的形式,即仅写一个 ".mod"文件 即可。

背包问题:
往一个承重为W的背包中,装入重量和价值都不同的物品。 每个物品的重量为wi,价值为vi。
问:应装哪几件物品,使得装入背包的物品总价值最高?

建立的数学模型为:

(其中,xi为决策变量,选第i个物品时为1,不选时为0)

同样按照" 参集变,mst "的步骤来编程吧。

参集变:
参数 是n、W、wi和vi; 集合 是1..n; 变量 是xi;
int n=4; // 假设是4个物品
int W=15; // 假设最大承重为15