掘金 后端 ( ) • 2024-04-27 09:38

简介

  • 这里的介绍取自官方文档:Language User’s Manual

  • The OPL (Optimization Programming Language)优化编程语言,是一种用于组合优化的建模语言,其目标在于简化优化问题的解算过程。线性规划、整数规划和组合优化问题体现在许多应用程序领域中,包括规划、调度、排序、资源分配、设计和配置。

  • 在上述提到的应用情形中,OPL是一种用于组合优化的建模语言,其目标在于简化这些优化问题的解算过程。 就其本身而言,它以相当于计算机的形式提供对建模线性、二次和整数程序的支持,并允许访问针对线性规划、数学整数规划和二次规划的最先进算法。

  • 简单来说,它和其它编程语言差不多,都是为了两个目的:方便人类进行计算高效率地进行建模,在不考虑其它语言特色(如OOP(面向对象),多线程,静态/动态语言)的情况下,它只是为了让妳和ILOG CPLEX Optimization Studio一起使用,仅此而已

从简单的语法开始

标识符

  • 我们常常会给生活中的各种物品命名,我们会为雨后天上划过的彩带称作“彩虹”,会把每天吃的早餐叫做“包子”和“粥”,它们就是我们作为中文的标识符

  • 标识符用于标记或给一个语言内的实体命名,就像这个例子:

a = 5
  • 这是python中一个很常见的例子,我们把数字5叫做a,这个a就是标识符。这里的=号可不能认作a和5相等,我感觉最好的方式是叫做。还记得《赛马娘》里东海帝皇最喜欢的饮料吗?是蜂蜜水哦,它的日语是はちみ,到中国变成了哈基米,再到抖音里我们看到这个词,很自然地能想到猫咪, 实际上二者的意思大相径庭。

  • 很神奇,是不是?而编程语言中的标识符有以下几个特点:

    • 用于给xx命名,它可以是字母,数字和下划线,一般情况下严格区分大小写,且不能是语言关键字,而且不以数字开头,保证连贯,中间没有空格

    • 标识符的作用范围是有限的,在不同的情况下可以让两个不一样的东西拥有相同的标识符

  • 先解释一下第一个:定义这样的一个规则是为了便于我们阅读,并且准确明白一个标识符的意思。我们可以看下面的例子:

zhang3 = 3
li4 = 4
wang5 = 5
  • 是吧,还是有一丢丢“一目了然”的意思在的

  • 再看第二个:上面我提到过的“哈基米”,其实就是一个很典型的例子

scope 中文
    ha_ji_mi = "没啥实际含义"
    scope 抖音
        ha_ji_mi = "猫猫"

scope 日语
    ha_ji_mi = "蜂蜜水"
  • 上述我写的例子不属于任何一种编程语言,只是一种表述方式,想让妳更好理解。看到了嘛,在中文且抖音中文日语这三种情况下,它表现出了完全不同的意思,这在编程语言的解释里是完全可行的。一般情况下会按就近原则解释。

  • 下面我举的例子都是合法的标识符:

apple # 纯数字
zhang3 # 字母+数字
Zhang3 # 注意大小写敏感,它和上面可以表达不同的意思
_private # 下划线+字母
...
  • 当然也有一些不合法的标识符:
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 只能用作决策变量的变量类型,值有10两种,分别表示 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),或者一个可以取值的盒子

  • 我们需要声明一个变量才能对它进行使用,比如下面的例子

int a;
float b;
string str;
  • 这里我们声明了三个变量,告诉计算机:有一个叫做aint类型的变量,一个叫做bfloat类型的变量,和一个叫做strstring类型的变量

  • 变量在声明时都会有默认值,上面我们声明的变量的值分别为:

    • a:0

    • b:0.0

    • str:""(引号用来表示它包裹的内容是一个字符串)

  • 我们可以通过赋值来改变它们的值

int a;
float b;
string str;
a = 5;
b = 5.0;
str = "hajimi";
  • 当然我们也可以一开始就进行赋值
int a = 5;
float b = 5.0;
string str = "hajimi";
  • 注意:在编程语言中单个=号表示将右边的赋给左边,而不是二者相等

  • 函数的介绍这里暂且略过

数据结构

  • 数据结构:结构化表示的数据。它其实也很形象,我们常用的excel就是以的形式展现数据的,它就是一个很经典的数据结构。还有妳学过的字典元组,这些,马上我就要介绍它们在OPL中是如何展现的

范围

  • 还记得python中的range吗?在python里的range(0,10)就是一个很经典的范围表示。在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就可以表示

  • 我们也可以这样声明一个区间:

int n = 10;
range R = n*10..n*100;
  • 这样我们就可以通过修改n的值,来快速声明一个[n*10,n*100]的区间

数组

  • 一个只要学习语言就无法避开的词汇。其实很多东西只是一个方便人类使用的工具,每当我们接触一个新东西,我们都需要提出一个问题:它的出现是为了解决什么问题?

  • 还记得我们前面提到过的变量声明吗?如果我们不需要关心水果的名字是什么,而只需要记住或者对它们的价格用来运算时,我们该如何声明/定义变量?

  • 一个个声明,像这样?

float apple,pineapple,pear;
  • 当需要我们进行决策的水果种类数很少时 ,这样的方式是没有问题的。虽然写起来确实很繁琐,但是我们一眼就能看出它们分别代表的是什么水果。

  • 但如果问题是以这样表述的:种类1的水果单价为¥2每千克,种类2的水果单价为¥3每千克,...,种类200的水果单价为¥201每千克,我们又该如何记下这些水果的单价?数组的出现就是为了解决这样的问题。

  • 数组同种类型的数据组成的集合,是有序的元素序列

  • 我们可以这样声明一个数组:

[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]来分别拿到这些元素的值。

  • 注意:数组中的元素一定是同种类型的,以下的形式是不被允许的:

[1,2,2.0,"你好世界"]
/*
    从左到右依次为:int,int,float,string
*/
  • a[]里的[]理论上可以放置任何数据结构,比如我们这里用的1..5就是一个范围

  • 我们还可以使用推导式来定义数组,生成有序的数据

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。该如何表示这样的数据?

  • 当然我们可以用数组表示:

int morning[ i in 1..200 ] = 10*i
int evening[ i in 1..200 ] = 5*i;
  • 但是这样的写法其实是和我们的认知相悖的:这两个价格作为水果的属性,他们应该依附在水果上,而不是单独拿出来进行赋值,总觉得怪怪的

  • 对于这样同属于某种类型的属性,我们希望它和这个类型进行绑定,而不是单独表示,这样可能会让人更容易理解。就像我们的身份证,上面写着我们的身份证号、姓名、年龄,它就是一个元组。

  • 元组将紧密相关的数据聚集在一起的数据结构

  • 我们用元组该如何表示水果的数据?

tuple Fruit{
    int morning_price;
    int evening_price;
}
Fruit goods[ i in 1..200 ] = <10*i,5*i>; 
  • 这里我们使用tuple这个关键字声明了一个名为Fruit的元组。它的属性有morning_priceeverning_price两个。随后,我们定义了一个类型为Fruit的数组用来装这些数据,i in 1..200对范围依次遍历,其后生成元组<10*i,5*i>,把这些值依次放到goods[1],goods[2]...goods[200]

  • 当我们需要得到指定元素的值时,我们可以通过引用的方式得到

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里的集合定义和数学差距不大,官方定义如下:

  • 集合没有重复内容的元素的未编制索引组合

  • 最重要的重点就是无重复元素,记住这点就差不多了。集合的一般声明方式如下

{int} setInt = { 1,2,3, }
setof(int) setInt = { 1,2,3 }
  • 两种方式声明的集合相同,下面的方式需要记忆setof关键字

集合的运算

union
  • 表示并集运算
{int} set1 = {1,2,3};
{int} set2 = {1,4,5,6};
{int} set3 = set1 union set2
  • 这里set3 = { 1,2,3,4,5,6 }
inter
  • 表示交集运算
{int} set1 = {1,2,3};
{int} set2 = {1,4,5,6};
{int} set3 = set1 inter set2
  • 这里set3 = { 1 }
diff
  • 表示集合的减法运算
{int} set1 = {1,2,3};
{int} set2 = {1,4,5,6};
{int} set3 = set1 diff set2;
  • 这里set3 = { 2,3 }
symdiff
  • 表示对两个集合作对称差
{int} set1 = {1,2,3,4,5};
{int} set2 = {1,4,5,6};
{int} set3 = set1 symdiff set2;
  • 这里set3 = { 2,3 }
已排列和排序的集合
  • 默认情况下我们创建集合,其实是已经指定了该集合是有序的
{int} S1 = { 3,2,1 };
ordered {int} S2 = { 3,2,1 };
  • 上面的两个句子是等价的

  • 这里ordered是一个关键字。表示其后声明的东西是排好序的

  • 如果我们需要把上面的集合升序排列,那么我们可以使用sorted

{int} S1 = { 1,2,3 };
sorted {int} S2 = { 3,2,1 };
  • 这两个句子等价,如果我们希望得到降序的序列,可以使用reversed
{int} S1 = { 1,3,2 };
sorted {int} S2 = S1;
reversed {int} S3 = S2;
  • 这里S3 = { 3,2,1 }

综合使用

集合作为数组索引

  • 我们可以创建集合作为数组的访问索引:
{string} S = { "apple","pineapple","peach" }
int fruit[S] = { 1,2,3 }
  • 在这种情况下,数组的各个元素分别为:

    • fruit["apple"] = 1

    • fruit["pineapple"] = 2

    • fruit["peach"] = 3

  • 我们也可以使用元组集合来取得数组的元素:

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

  • 但是如果要取得相应的元素,我们需要写出一整个元组,未免太过麻烦,我们可以用来简化这个访问过程

元组的键
  • 在不写出整个元组的情况下,我们怎样才能用一个唯一的值来获得指定的元组?就像我们的身份证号,只要拿到这个唯一的身份证号,公安系统自然就可以查到我们的人脸、生日、住址、年龄等等信息,这无疑方便得多

  • 我们可以这样通过来作为索引访问数组元素:

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">];
  • 当然我们可以定义不止一个,在多键的情况下,我们需要保证这些全部明确才能访问到我们想要的元素
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">];
已排列和排序的元组集合
  • 如果元组集合不使用键,那么会将整个元组除了固定字段和数组字段)都考虑到排列操作中。 对于具有键的元组集,按照键的声明顺序基于所有键进行排列。 也就是说,无法只在一个(或多个)给定列上对元组集合进行排列。

  • 这样一大段文字太过抽象了,我们看下面的例子:

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,所以最后的结果如上所示

  • 对于有的集合:

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的属性如下:
tuple Object{
    string name;
}
  • 我们有什么更好的办法来实现类似这样的集合的转换?

  • 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

  • 实际上objects1objects2是完全等价的,即属性的值只按顺序对应,和标识符无关

最后的最后

  • 宝宝,这些知识虽然很基础很简单,但还是希望妳能牢牢记住。下一篇我才会开始讲解一些基础代码的编写,加油~~