简介
-
这里的介绍取自官方文档:Language User's Manual
-
The
OPL
(Optimization Programming Language) 即优化编程语言 ,是一种用于组合优化的建模语言,其目标在于简化优化问题的解算过程。线性规划、整数规划和组合优化问题体现在许多应用程序领域中,包括规划、调度、排序、资源分配、设计和配置。 -
在上述提到的应用情形中,
OPL
是一种用于组合优化的建模语言,其目标在于简化这些优化问题的解算过程。 就其本身而言,它以相当于计算机的形式提供对建模线性、二次和整数程序的支持 ,并允许访问针对线性规划、数学整数规划和二次规划的最先进算法。 -
简单来说,它和其它编程语言差不多,都是为了两个目的:方便人类进行计算 和高效率地进行建模 ,在不考虑其它语言特色(如OOP (面向对象),多线程 ,静态/动态语言 )的情况下,它只是为了让妳和
ILOG CPLEX Optimization Studio
一起使用,仅此而已
从简单的语法开始
标识符
-
我们常常会给生活中的各种物品命名,我们会为雨后天上划过的彩带称作"彩虹",会把每天吃的早餐叫做"包子"和"粥",它们就是我们作为中文的标识符
-
标识符用于标记或给一个语言内的实体命名,就像这个例子:
python
a = 5
-
这是
python
中一个很常见的例子,我们把数字5
叫做a
,这个a
就是标识符。这里的=
号可不能认作a和5相等 ,我感觉最好的方式是叫做 。还记得《赛马娘》里东海帝皇最喜欢的饮料吗?是蜂蜜水 哦,它的日语是はちみ
,到中国变成了哈基米 ,再到抖音里我们看到这个词,很自然地能想到猫咪, 实际上二者的意思大相径庭。 -
很神奇,是不是?而编程语言中的标识符有以下几个特点:
-
用于给xx命名,它可以是字母,数字和下划线 ,一般情况下严格区分大小写 ,且不能是语言关键字 ,而且不以数字开头 ,保证连贯,中间没有空格
-
标识符的作用范围是有限的 ,在不同的情况下 可以让两个不一样的东西拥有相同的标识符
-
-
先解释一下第一个:定义这样的一个规则是为了便于我们阅读,并且准确明白一个标识符的意思。我们可以看下面的例子:
python
zhang3 = 3
li4 = 4
wang5 = 5
-
是吧,还是有一丢丢"一目了然"的意思在的
-
再看第二个:上面我提到过的"哈基米",其实就是一个很典型的例子
python
scope 中文
ha_ji_mi = "没啥实际含义"
scope 抖音
ha_ji_mi = "猫猫"
scope 日语
ha_ji_mi = "蜂蜜水"
-
上述我写的例子不属于任何一种编程语言 ,只是一种表述方式,想让妳更好理解 。看到了嘛,在中文且抖音 ,中文 ,日语 这三种情况下,它表现出了完全不同的意思,这在编程语言的解释里是完全可行的。一般情况下会按就近原则解释。
-
下面我举的例子都是合法的标识符:
python
apple # 纯数字
zhang3 # 字母+数字
Zhang3 # 注意大小写敏感,它和上面可以表达不同的意思
_private # 下划线+字母
...
- 当然也有一些不合法的标识符:
python
Xi an # 中间不能有空格
Your's # 有特殊字符,不被允许
啊~我的祖国 # 不是字母,且有特殊字符
520you # 数字开头,不行捏
...
-
关于语言关键字 :编程语言里有些名字是很特殊的,它们不能被用来命名。还记得妳学过一丢丢的
python
嘛,里面在导入包的时候是不是得import
一下,这里的import
就不能用来命名 -
以下是
OPL
中一些关键字,不需要特别记忆,因为之后的教程里会频繁的使用,自然就会记住(挺长的,不想看就往下拉)
关键字 | 描述 |
---|---|
all | 允许仅将数组的一部分与采用数组参数的函数一起使用。 |
and | CP。 使用逻辑 AND 将多个约束聚合为一个约束。 |
assert | 检查假定。 |
boolean | 决策变量的域快捷方式。 |
constraints | 约束 (subject to) 的别名。 |
CP | 表示约束规划模型。 |
CPLEX | 表示数学规划模型。 |
cumulFunction | 用于表示累积函数(CP 关键字,调度)。 |
dexpr | 以更加紧凑的方式表示决策变量。 |
diff | 两个数据集的差异。 |
div | 整数除法运算符。 |
dvar | OPL 模型中的决策变量。 |
else | 用于声明条件约束。 |
execute | 引入预处理或后处理脚本编制块。 |
false | 始终为 false 的约束的快捷方式。 |
float | 声明浮点值。 |
float+ | 决策变量的域快捷方式。 |
forall | 引入约束。 |
from | 与 DBRead 和 SheetRead 关键字有关,用于从数据库或电子表格读取数据。 |
in | 检查集中的成员资格。 |
if | 用于声明条件约束 |
include | 将一个模型包含到另一个模型中。 |
infinity | 用于表示 IEEE 无穷大符号的预定义浮点常数。 |
int | 声明整数。 |
int+ | 决策变量的域快捷方式。 |
intensity | 用于定义区间的强度(CP 关键字,调度)。 |
inter | 保留数据集之间的公共元素(交集)。 |
[ ] | 用于创建区间变量(CP 关键字,调度)。 |
invoke | 在数据初始化后调用 IBM ILOG Script 函数。 |
key | 在声明元组时,使您能够使用一组唯一标识来访问以元组形式组织的数据。 |
main | 引入流控制脚本编制块。 |
max | 计算相关表达式集合的最大值。 |
maximize | 用于表示目标函数的约束。 |
maxint | OPL 中可用的最大正整数。 |
min | 计算相关表达式集合的最小值。 |
minimize | 用于表示目标函数的约束。 |
mod | 整数除法的余数。 |
not in | 集中的非成员资格。 |
optional | 用于将区间声明为可选(CP 关键字,调度)。 |
or | CP。 使用逻辑 OR 将多个约束聚合为一个约束。 |
ordered | 组合多个参数来产生更紧凑的语句。 |
piecewise | 引入连续和不连续分段线性函数。 |
prepare | 引入要在 .dat 文件的某个其他部分中使用的 IBM ILOG Script 函数定义。 |
prod | 计算相关表达式集合的积。 |
pwlFunction | 用于对时间的已知连续函数建模(调度)。 |
range | 通过下界和上界定义整数范围。 |
reversed | 指定集中的词典式降序。 |
sequence | 用于定义区间变量的序列(CP 关键字,调度)。 |
setof | 定义集(唯一元素的列表)。 |
SheetConnection | 将模型连接到电子表格。 |
SheetRead | 从电子表格中读取数据。 |
SheetWrite | 将数据写入电子表格。 |
size | 用于定义区间大小(CP 关键字,调度)。 |
sorted | 按词典式的自然升序对集排序。 |
stateFunction | 用于表示状态函数(CP 关键字,调度)。 |
stepFunction | pwlFunction的一种特殊用例,其中该函数在分步区间中会发生变化(调度)。 |
stepwise | 用于表示分步线性函数(调度)。 |
string | 声明数据字符串。 |
subject to | 引入优化指令,后跟约束块。 |
sum | 计算相关表达式集合的和。 |
symdiff | 运行两个集的并集和交集的差异。 |
to | 与 DBUpdate 和 SheetWrite 关键字有关,用于将数据写入数据库或电子表格。 |
true | 始终为 true 的约束的快捷方式。 |
tuple | 用于将紧密相关的数据聚集在一起的数据结构。 |
types | 用于将非负整数(区间变量类型)与序列中的每个区间变量关联起来。 |
union | 将集的不相同元素添加到其他集。 |
using | 与关键字 CP 或 CPLEX 关联,用于为模型指定解算引擎。 |
with | 指示元组的元素必须包含在给定集中。 |
基本数据类型
类型 | 描述 |
---|---|
boolean | 只能用作决策变量 的变量类型,值有1 和0 两种,分别表示真 和假 |
float | 浮点数,小数 |
float+ | 非负浮点数 |
int | 整数 |
int+ | 非负整数 |
string | 字符串,即用引号表示的我们所知的所有字符,"apple","你好"这些 |
- 对于这些已经在编程语言中被构造好的,方便我们直接使用的类型,我们称之为基础数据类型 ,基础数据类型还有分段线性函数 和分布函数,在后续的使用中我会对它们进行更详细的介绍
int
- 表示整数类型 ,我们可以使用
maxint
来获得opl
支持的最大整数的范围 opl
支持的int
数据范围在-2147483647~2147483647
之间
float
- 表示浮点数(小数)类型 ,我们可以使用
infinity
来获得opl
支持的最大浮点数的范围 infinity
一般用来表示无穷大
string
- 表示字符串类型 ,可以用来包含计算机上的所有字符。但是有一些字符有特殊用途,它们以
\
开头,且不能被显示。这些字符被称为转义字符
转义序列 | 含义 |
---|---|
\b | 退格 |
\t | 制表符 |
\n | 换行符 |
\f | 换页 |
\r | 回车符 |
" | 双引号 |
\ | 反斜杠 |
\ooo | 八进制字符 ooo |
\xXX | 十六进制字符 XX |
- 我们所看到的所有换行效果,在
windows
下都是\r\n
这两个字符实现的效果
声明 | 赋值
-
这些数据类型需要结合变量 和函数才能正常使用
-
变量 :可变的量,它与常量相对
-
我们常在编程语言中使用变量 来对值进行保存,运算和输出 ,可以把它理解为一个代数符号 (如解方程的
x
),或者一个可以取值的盒子 -
我们需要声明一个变量才能对它进行使用,比如下面的例子
opl
int a;
float b;
string str;
-
这里我们声明 了三个变量,告诉计算机:有一个叫做
a
的int
类型的变量,一个叫做b
的float
类型的变量,和一个叫做str
的string
类型的变量 -
变量在声明时都会有默认值,上面我们声明的变量的值分别为:
-
a:0
-
b:0.0
-
str:""
(引号用来表示它包裹的内容是一个字符串)
-
-
我们可以通过赋值来改变它们的值
opl
int a;
float b;
string str;
a = 5;
b = 5.0;
str = "hajimi";
- 当然我们也可以一开始就进行赋值:
opl
int a = 5;
float b = 5.0;
string str = "hajimi";
-
注意 :在编程语言中单个
=
号表示将右边的值 赋给左边,而不是二者相等 -
函数的介绍这里暂且略过
数据结构
- 数据结构 :结构化表示的数据。它其实也很形象,我们常用的
excel
就是以表 的形式展现数据的,它就是一个很经典的数据结构。还有妳学过的字典 ,元组 ,这些,马上我就要介绍它们在OPL
中是如何展现的
范围
- 还记得
python
中的range
吗?在python
里的range(0,10)
就是一个很经典的范围表示。在opl
中,我们可以这样表示一个范围:
opl
1..10;
range rows = 1..10;
-
第一行:
1..10
表示一个只包含整数的闭区间:[1,10]
,里面一共有10
个整数:1,2,3,4,5,6,7,8,9,10
, -
第二行:我们在等式的右边写出 了一个区间:
[1,10]
,并把它的值赋给左边,保存在变量rows
中。在这里range
就是一个关键字 ,当我们需要保存一个区间的值时可以采用这种形式,以便后续在使用相同区间时可以用一个相同的rows
就可以表示 -
我们也可以这样声明一个区间:
opl
int n = 10;
range R = n*10..n*100;
- 这样我们就可以通过修改
n
的值,来快速声明一个[n*10,n*100]
的区间
数组
-
一个只要学习语言就无法避开的词汇。其实很多东西只是一个方便人类使用的工具 ,每当我们接触一个新东西,我们都需要提出一个问题:它的出现是为了解决什么问题?
-
还记得我们前面提到过的变量声明吗?如果我们不需要关心水果的名字 是什么,而只需要记住 或者对它们的价格用来运算 时,我们该如何声明/定义变量?
-
一个个声明,像这样?
opl
float apple,pineapple,pear;
-
当需要我们进行决策的水果种类数很少时 ,这样的方式是没有问题的。虽然写起来确实很繁琐,但是我们一眼就能看出它们分别代表的是什么水果。
-
但如果问题是以这样表述的:种类1的水果单价为¥2每千克,种类2的水果单价为¥3每千克,...,种类200的水果单价为¥201每千克,我们又该如何记下这些水果的单价?数组的出现就是为了解决这样的问题。
-
数组 :同种类型 的数据组成的集合 ,是有序的元素序列
-
我们可以这样声明一个数组:
opl
[10,20,30,40];
int a[1..5] = [10,20,30,40,50];
-
第一行:声明了一个长度为
4
的数组,里面的元素为10
,20
,30
,40
-
第二行:声明了一个长度为
5
的数组,里面的元素为10
,20
,30
,40
,50
,并把它保存在a
的整数数组中。我们可以通过a[1]
,a[2]
,a[3]
,a[4]
,a[5]
来分别拿到这些元素的值。 -
注意 :数组中的元素一定是同种类型的,以下的形式是不被允许的:
opl
[1,2,2.0,"你好世界"]
/*
从左到右依次为:int,int,float,string
*/
-
a[]
里的[]
理论上可以放置任何数据结构,比如我们这里用的1..5
就是一个范围 -
我们还可以使用推导式来定义数组,生成有序的数据
opl
int a[ i in 1..10] = i
int a[ 1..10 ] = [ 1,2,3,4,5,6,7,8,9,10 ]
- 这里我们使用了关键字
in
,这里变量 i range
的用法表示i
这个变量遍历1..10
这个区间 ,也就是说,它依次取值为1,2,3,4,5,6,7,8,9,10
,我们看右边,我们将每次得到的i
依次 赋给了a[1],a[2],a[3],a[4]...a[10]
,最后我们得到的数组和下面是一模一样的。这样的写法刚开始看的时候可能会难以理解,但是熟悉之后你会发现其实这样的写法确实省去了很多时间
元组
-
我们前面解决了同类数据过多时,该如何进行表示和计算的问题。现在让问题变得更复杂些:种类1的水果早上价格为¥10,晚上价格为¥5,种类2的水果早上价格为¥20,晚上价格为¥10,...,种类200的水果早上价格为¥2000,晚上价格为¥1000。该如何表示这样的数据?
-
当然我们可以用数组表示:
opl
int morning[ i in 1..200 ] = 10*i
int evening[ i in 1..200 ] = 5*i;
-
但是这样的写法其实是和我们的认知相悖的:这两个价格作为水果的属性 ,他们应该依附在水果上,而不是单独拿出来进行赋值,总觉得怪怪的
-
对于这样同属于某种类型的属性 ,我们希望它和这个类型进行绑定,而不是单独表示,这样可能会让人更容易理解。就像我们的身份证,上面写着我们的身份证号、姓名、年龄,它就是一个元组。
-
元组 :将紧密相关的数据聚集在一起的数据结构
-
我们用元组该如何表示水果的数据?
opl
tuple Fruit{
int morning_price;
int evening_price;
}
Fruit goods[ i in 1..200 ] = <10*i,5*i>;
-
这里我们使用
tuple
这个关键字声明了一个名为Fruit
的元组。它的属性有morning_price
和everning_price
两个。随后,我们定义了一个类型为Fruit
的数组用来装这些数据,i in 1..200
对范围依次遍历,其后生成元组<10*i,5*i>
,把这些值依次放到goods[1],goods[2]...goods[200]
中 -
当我们需要得到指定元素的值时,我们可以通过引用的方式得到
opl
tuple Fruit{
int morning_price;
int evening_price;
}
Fruit goods[ i in 1..200 ] = <10*i,5*i>;
int morning = goods[1].morning_price;
int evening = goods[1].morning_price;
-
在这里
goods[1]
得到了这个数组的第一个元组,.morning_price
表示获取这个元组的morning_price
这个属性,这样我们就可以得到数组中指定元组的属性值。 -
注意:元组中的数组和集合不按内容进行比较。 如果元组内部的集合已修改,那么不会检测到重复内容(集合的内容在下一点)
元组限制
-
不是所有的数据类型都能在元组中表示,允许的有:
-
基础数据类型(int、float 和 string)
-
元组(子元组)
-
拥有基础数据类型的数组(不能是字符串数组)
-
集合
-
-
以下的是不被允许的
-
元组集合
-
字符串、元组和元组集合的数组
-
多维数组
-
集合
-
OPL
里的集合定义和数学差距不大,官方定义如下: -
集合 :没有重复内容的元素的未编制索引组合
-
最重要的重点就是无重复元素,记住这点就差不多了。集合的一般声明方式如下
opl
{int} setInt = { 1,2,3, }
setof(int) setInt = { 1,2,3 }
- 两种方式声明的集合相同,下面的方式需要记忆
setof
关键字
集合的运算
union
- 表示并集运算
opl
{int} set1 = {1,2,3};
{int} set2 = {1,4,5,6};
{int} set3 = set1 union set2
- 这里
set3 = { 1,2,3,4,5,6 }
inter
- 表示交集运算
opl
{int} set1 = {1,2,3};
{int} set2 = {1,4,5,6};
{int} set3 = set1 inter set2
- 这里
set3 = { 1 }
diff
- 表示集合的减法运算
opl
{int} set1 = {1,2,3};
{int} set2 = {1,4,5,6};
{int} set3 = set1 diff set2;
- 这里
set3 = { 2,3 }
symdiff
- 表示对两个集合作对称差
opl
{int} set1 = {1,2,3,4,5};
{int} set2 = {1,4,5,6};
{int} set3 = set1 symdiff set2;
- 这里
set3 = { 2,3 }
已排列和排序的集合
- 默认情况下我们创建集合,其实是已经指定了该集合是有序的
opl
{int} S1 = { 3,2,1 };
ordered {int} S2 = { 3,2,1 };
-
上面的两个句子是等价的
-
这里
ordered
是一个关键字。表示其后声明的东西是排好序的 -
如果我们需要把上面的集合升序排列 ,那么我们可以使用
sorted
opl
{int} S1 = { 1,2,3 };
sorted {int} S2 = { 3,2,1 };
- 这两个句子等价,如果我们希望得到降序的序列,可以使用
reversed
opl
{int} S1 = { 1,3,2 };
sorted {int} S2 = S1;
reversed {int} S3 = S2;
- 这里
S3 = { 3,2,1 }
综合使用
集合作为数组索引
- 我们可以创建集合作为数组的访问索引:
opl
{string} S = { "apple","pineapple","peach" }
int fruit[S] = { 1,2,3 }
-
在这种情况下,数组的各个元素分别为:
-
fruit["apple"] = 1
-
fruit["pineapple"] = 2
-
fruit["peach"] = 3
-
-
我们也可以使用元组集合来取得数组的元素:
opl
tuple Fruit{
key string name;
int morning_price;
int evening_price;
}
{Fruit} fruit = { <"apple",100,20> , <"pear",200,30>,<"hajimi",12,2> };
int total[fruit] = [ 120,230,14 ];
-
这样数组的各个元素为:
-
fruit[<"apple",100,20>] = 120
-
fruit[<"pear",200,30>] = 230
-
fruit[<"hajimi",12,2>] = 14
-
-
但是如果要取得相应的元素,我们需要写出一整个元组,未免太过麻烦,我们可以用键来简化这个访问过程
元组的键
-
在不写出整个元组的情况下,我们怎样才能用一个唯一的值来获得指定的元组 ?就像我们的身份证号,只要拿到这个唯一的身份证号,公安系统自然就可以查到我们的人脸、生日、住址、年龄等等信息,这无疑方便得多
-
我们可以这样通过键 来作为索引访问数组元素:
opl
tuple Fruit{
key string name;
int morning_price;
int evening_price;
}
{Fruit} fruit = { <"apple",100,20> , <"pear",200,30>,<"hajimi",12,2> };
int total[ fruit ] = [120,230,14];
int total_apple = total[<"apple">];
- 当然我们可以定义不止一个键 ,在多键 的情况下,我们需要保证这些键全部明确才能访问到我们想要的元素
opl
tuple Fruit{
key string name;
key string gender;
int morning_price;
int evening_price;
}
{Fruit} fruit = { <"apple","male",100,20> , <"pear","female",200,30>,<"hajimi","unknown",12,2> };
int total[ fruit ] = [120,230,14];
int total_apple = total[<"apple","male">];
已排列和排序的元组集合
-
如果元组集合不使用键,那么会将整个元组 (除了固定字段和数组字段 )都考虑到排列操作中。 对于具有键的元组集,按照键的声明顺序基于所有键进行排列 。 也就是说,无法只在一个(或多个)给定列上对元组集合进行排列。
-
这样一大段文字太过抽象了,我们看下面的例子:
opl
tuple Fruit{
string name;
int morning_price;
int evening_price;
}
sorted {Fruit} fruit = { <"pineapple",100,100>,<"hajimi",50,50> ,<"apple",500,500> };
-
最后得到的集合为:
{<"apple" 500 500>,<"hajimi" 50 50>,<"pineapple" 100 100>}
,这里它为我们自动定好了排列顺序:取第一个属性,a<h<p
,所以最后的结果如上所示 -
对于有键的集合:
opl
tuple Fruit{
string name;
key int morning_price;
int evening_price;
}
sorted {Fruit} fruit = { <"pineapple",100,100>,<"hajimi",50,50> ,<"apple",500,500> };
- 运行结果为:
{<"hajimi" 50 50> <"pineapple" 100 100> <"apple" 500 500>}
,我们在规定键 后,相当于为它们的排序定下了排序的规则 ,sorted
默认得到的结果为升序排列,由于morning_price
属性中50<100<500
,所以呈现这样的结果
集合的转化
- 现在我们又抛出一个问题:我想把
fruit
这个元组集合里的元素放到另一个新的叫做Object
的元组集合里,而这个Object
的属性如下:
opl
tuple Object{
string name;
}
-
我们有什么更好的办法来实现类似这样的集合的转换?
-
opl
用一种极其接近数学语言的方式来完成这样的需求:
opl
tuple Object{
string name;
}
tuple Fruit{
string name;
key int morning_price;
int evening_price;
}
{Fruit} fruits = { <"pineapple",100,100>,<"hajimi",50,50> ,<"apple",500,500> };
{Object} objects1 = { <name> | <name,morning_price,evening_price> in fruits };
{Object} objects2 = { <i> | <i,j,k> in fruits };
-
最后
objects1
的结果为:{<"pineapple"> <"hajimi"> <"apple">}
-
这里的
<name,morning_price,evening_price>
表示取出fruits
的一个元组,<name>
表示生成一个新元组,这个元组的形式为<name>
,因为在Object
这个元组类型中我们只需要第一个属性。最后通过这个推导式 得到的集合我们赋值 给了objects1
-
实际上
objects1
和objects2
是完全等价的,即属性的值只按顺序对应,和标识符无关
最后的最后
- 宝宝,这些知识虽然很基础很简单,但还是希望妳能牢牢记住。下一篇我才会开始讲解一些基础代码的编写,加油~~