OPL语言学习
参考B站视频:cplex入门到精通
相关下载资源放在公众号【诸葛小猿】(现在已经更名【角色棱镜】,换了写作方向,因为下载的人多,还是保留了关键字“cplex”下载)上,关注即可获得下载链接。
创建OPL建模语言,是为了简化对数学规划问题的解算。 许多数学规划问题都可以使用计算机语言表达,其语法与这些问题在教科书和科学论文中的标准表示法相似。
1.注释
注释的作用:提高代码的可读性。
注释是给人看的,程序执行时会忽略注释。
1.1.单行注释
快捷键: ctrl + /
// 单行注释 独占一行
int a = 4; // 单行注释 在代码的结尾
1.2.多行注释
/*
这个是0-1背包问题的模型。
使用注意:......
说明:....
作者:.....
*/
2.OPL说明
OPL 的基本构建块是整数、浮点数、字符串、标识和关键字。
OPL 中的标识只能包含字母、数字和下划线,并且不能以数字开始。
OPL 中的字母区分大小写。
OPL 模型包括:
- 变量声明
- 可选预处理指令
- 模型/问题定义
- 可选处理后指令
- 可选流控制(
main
块)
3.基本数据类型
基本数据类型包括:整数、浮点数、字符串、分段线性函数和分步函数。
3.1.整数int
整数是数字序列,可能带有减号前缀。
整数常量 maxint,它表示可用的最大正整数。
OPL 提供范围整数范围: -maxint
到 maxint
。
int a; // 定义 未初始化 默认为0
int b = -3; // 定义并初始化
int c = a*a; // 通过表达式初始化整数 c 初始化为a 的平方
int d = maxint; // 最大整数 2147483647
int e = -maxint;// 最小整数 -2147483647
execute{
writeln(a," ",b," ",c," ",d," ",e); // 0 -3 0 2147483647 -2147483647
}
3.2.浮点数float
OPL 中的数据类型 float
使用双精度浮点数。
预定义的浮点常数 infinity
,用于表示无穷大符号。
浮点数可以用小数符号(3.4
或 -2.5
) 或使用科学记数法(3.5e-3
或 -3.4e10
)进行描述。
float a; // 只定义 不初始化 默认为0
float b = 3.14; // 定义并初始化
float c = -3.1415926e8; // 科学计数法
float d = infinity; // 正无穷大
float e = -infinity; // 负无穷大
float f = 2.5*a + b; // 通过表达式初始化float
execute{
writeln(a," ", b," ",c," ",d," ",e," ",f); // 0 3.14 -314159260 Infinity -Infinity 3.14
}
3.3.字符串string
转义字符\
string a;
string b = "";
string c = "cplex";
string d = "sss\"dddd\n"; // 转义字符 \"
string e = "id\tname\tage\taddr\n1\twxl\t30\tshanghai\n"; // 转义字符 \t \n
execute{
writeln(a," ",b," ",c," ",d,"",e);
}
执行结果:
cplex sss"dddd
id name age addr
1 wxl 30 shanghai
4.数据结构
使用整数、浮点数、字符串等基本类型构建的更复杂的数据结构。
范围(range)、数组(array)、元组(tuple)和集合(set)
4.1范围range
范围运算符:…。
上界小于下界,范围为空。空范围自动规范化为 0..-1
。所有空范围都相等。
4.1.1 整数range
要指定整数范围,可以给出其下界和上界,上界和下界必须都为整数。步长为1。
作用:
1.数组初始化时,作为数组索引;arr[R]
2.用作迭代范围。for(i in R) forall (i in R)
3.用作整数决策变量的定义域。dvar int i in R;
4.1.2.浮点型range
定义:range float X = 1.0…100.0;
作用:通常用作浮点决策变量的定义域: dvar float x in X;
range a; // 定义 不初始化 默认为范围为空 0..-1
range b = 1..5; // 整数范围,下界和上界
range c = -5..6;
range d = 9..3; // 空范围 0..-1
//range f = 1.0..5.0;// 语法错误 上下界必须为整数
int n = 8;
range e = n+1..2*n+1; // 通过表达式来给出下界和上界
range R = 1..100;
int arr[R]; // 数组arr有100个元素,元素的索引为1到100
range float g = -2.1..3.8; // 浮点数类型的range
execute{
writeln(a);
writeln(b);
writeln(c);
writeln(d);
writeln(e);
for(i in b){
writeln(i);
}
// for (j in g){ // 浮点数类型 不能循环打印
// writeln(j);
// }
}
执行结果:
0..-1
1..5
-5..6
0..-1
9..17
1
2
3
4
5
4.2.数组array
分一维数组和多维数组。
符号:[ ]
4.1.一维数组
一维数组在 OPL 中时最简单的数组,根据元素和索引集的类型而有所不同。
OPL 中的数组的索引可以是整数range或其他任意有限集合。
int a[1..4]; // 只定义 不初始化 默认[0,0,0,0] // 使用整形range作为数组的索引
int b[1..4] = [10, 20, 30, 40]; // 整数数组
range r = 1..4;
float c[r] = [1.2, 2.3, 3.4, 4.5]; // 将range定义和数组定义分开 浮点数数组
string d[1..2] = ["Monday", "Wednesday"]; // 字符串数组
{string} name = {"zhangsan","lisi","wangwu"}; // 定义一个字符串集合
float salary[name] = [1000.0,2000,8000]; // 以字符串集合为索引 非常有用
execute{
writeln(a);
writeln(b);
writeln(c);
writeln(d);
writeln(salary);
writeln(salary["zhangsan"]);
// writeln(salary["zhaoliu"]); // 查询不存在的索引对应的元素 报错
for(i in r){
writeln(c[i]) // 根据索引访问每一个元素
}
}
结果:
[1.2 2.3 3.4 4.5]
["Monday" "Wednesday"]
[1000 2000 8000]
1000
1.2
2.3
3.4
4.5
4.2.多维数组
声明:
int a[1..2][1..3] = ...;
元素表示:
元素形式为 a[index1][index2] 的二维数组
4.3.元组tuple
4.3.1.元组的定义和使用
元组可以将紧密相关的数据聚集在一起。类似于面向对象的类class。
符号:< >
声明及初始化:
tuple Point {
key int id;
int x;
int y;
}; // 定义元组
Point dot = <1,2>; // 初始化一个元组
Point point[i in 1..3] = <i, i+1>; // 初始化多个元组
{Point} set ={<1,1,2>,<2,3,4>}
一旦声明了元组类型 T
,便可以定义元组数组、元组集合、元组的元组。元组中不支持多维数组。
传统方式访问元组的各个字段,可以在元组名称中添加点和字段名称后缀。
元组结构可以与键关联。 元组键能够访问元组中使用一组唯一标识来组织的数据。
使用关键字:key
单个键: 只有一个key字段。key是唯一键。
多个键:有多个key字段。多个key字段合起来后是唯一键
在元组声明key的作用:
- 键字段可以用作元组的唯一标识。元组集合中的元素的key不能重复,否则 OPL 会报错。
- 定义key后,可以仅使用key字段的值来访问元组集合的元素。 可以仅使用键字段来对元组集合进行分类。
tuple Point {
int x;
int y;
} // 声明一个元组
Point dot = <1,2>; // 初始化一个元组对象
Point point[i in 1..3] = <i, i+1>; // 初始化一个元组数组,包含3个元组对象
{Point} points = {<1,2>, <2,3>}; // 初始化一个元组集合,包含2个元组对象
tuple Rectangle {
Point ll;
Point ur;
}// 定义一个元组的元组
Rectangle rect = <<3,4>,<5,6>>; // 初始化一个元组为元素的元组
tuple KeyPoint1 {
key int id;
int x;
int y;
}; // 声明含有单个key的元组
tuple Point {
int x;
int y;
} // 声明一个元组
Point dot = <1,2>; // 初始化一个元组对象
Point point[i in 1..3] = <i, i+1>; // 初始化一个元组数组,包含3个元组对象
{Point} points = {<1,2>, <2,3>}; // 初始化一个元组集合,包含2个元组对象
tuple Rectangle {
Point ll;
Point ur;
}// 定义一个元组的元组
Rectangle rect = <<3,4>,<5,6>>; // 初始化一个元组为元素的元组
tuple KeyPoint1 {
key int id;
int x;
int y;
}; // 声明含有单个key的元组
KeyPoint1 tup1 = <11,1,1>;
KeyPoint1 tup2 = <22,2,2>;
KeyPoint1 tup3 = <33,3,3>;
KeyPoint1 tup11 = <11,1,1>;
{KeyPoint1} kpSet = {tup1,tup2,tup3}; // 元组集合中的元素的key如果重复则下面arr运行时报错
int arr[kpSet] = [100,200,300]; // 元组作为数组的索引
tuple KeyPoint2 {
key int id;
key string name;
int x;
int y;
}; // 声明含有两个key的元组
//dvar int x;
//dexpr int y[t in kpSet] = 3*x; // 元组索引
//subject to {
// forall(<a,b,c> in kpSet){ // 不能元组模式 也要用元组索引 forall(t in kpSet){
// y[<a,b,c>]==4;
// }
//};
execute{
writeln(dot);
writeln(point);
writeln(points);
writeln(rect);
writeln(dot.x); // 访问单个字段
writeln(point[1].x);
writeln(rect.ll.x);
writeln(kpSet.get(33)) // 只通过key=33获取元组集合的元素
for(n in kpSet){ // n是元组索引
writeln(arr[n]);
}
writeln(arr[tup1]); // 可以只使用元组的key获取元组数组的值
writeln(arr[tup11]); // 可以只使用元组的key获取元组数组的值
// writeln(arr[<11,1,1>]); // 不能使用arr[<11,1,1>]
// writeln(arr[<11>]); // 不能使用arr[<11>]
}
// 结果:
<1 2>
[ <1 2> <2 3> <3 4>]
{<1 2> <2 3>}
<<3 4> <5 6>>
1
1
3
<33 3 3>
100
200
300
100
100
4.3.2元组中的数据类型限制
1.元组中允许的数据类型
- 原语(int、float 和 string)
- 元组(也称为子元组)
- 具有原语项的数组(非字符串),即:整数或浮动数组
- 具有原语项的集合,即:整数、浮点数或字符串集
2.元组中不允许的数据类型
- 元组集
- 字符串、元组和元组集的数组
- 多维数组
不能将声明中的元组索引和模式与决策表达式dexpr 混合使用:
dexpr float y[i in t] = ...; // 元组索引
subject to {
forall(<a,b,c> in t) y[<a,b,c>]==...; }; // 元组模式
或
dexpr float y[<a,b,c> in t] = ...; // 元组模式
subject to {
forall(i in t) y[i]==...; // 元组索引
};
如果选择在大型模型中标注约束,请使用元组索引来代替元组模式,从而避免增加性能和内存开销。
4.4.集合set
4.4.1.集合定义和初始化
集合set:没有重复元素(重复元素只保留一份),没有索引,默认已排序,顺序是创建顺序。
支持使用任意类型的集。
符号:{ }
定义:{type} 或 setof(type)
默认情况下,集合是已排序状态(ordered),意味着:
- 其元素顺序被视为创建顺序。
- 应用于有序集的函数和运算保留此顺序。
/*
1.集合的定义和初始化
*/
{string} nameSet; // 定义方式一: {type} 默认是空集{}
setof(string) citySet; // 定义方式二: setof(type) 默认是空集{}
{string} nameSet1 = {"zhangsan","lisi","wangwu"}; // 定义并初始化
setof(string) citySet1 = {"shanghai","beijing","shengzhen"}; // 定义并初始化
ordered {string} nameSet2 = {"zhangsan","lisi","wangwu"}; // 定义并初始化 上面默认都是 ordered: 默认集合是已排序状态,元素顺序为创建顺序
ordered setof(string) citySet2 = {"shanghai","beijing","shengzhen"};
{string} nameSet3 = {"zhangsan","wangwu","zhangsan","lisi","wangwu"}; // 定义并初始化 重复元素只保留一次
tuple Point {
int before;
int after;
}
{Point} points = {<1,2>, <1,3>, <3,4>}; // 定义并初始化 集合内部可以是任意类型的元素
execute{
writeln(nameSet);
writeln(citySet);
writeln(nameSet1);
writeln(citySet1);
writeln(nameSet2);
writeln(citySet2);
writeln(nameSet3);
writeln(points);
for(i in points){
writeln(i); // 查看每一个集合的元素
}
}
4.4.2.集合运算
在集合上允许以下运算:union、inter、diff 和 symdiff、first 和 last、next 和 prev、nextc 和 prevc、item、ord。 对于集上的函数,索引从 0 开始。
/*
2.集合的运算
*/
{string} nameSet1 = {"zhangsan","lisi","wangwu"};
setof(string) citySet3 = {"beijing","shengzhen"};
setof(string) citySet4 = {"shanghai","beijing"};
{string} unionset = citySet3 union citySet4; // 并集 {"beijing" "shengzhen" "shanghai"}
{string} interset = citySet3 inter citySet4; // 交集 {"beijing"}
{string} diffset = citySet3 diff citySet4; // 差集:第一个集合独有的元素 {"shengzhen"}
{string} symdiffset = citySet3 symdiff citySet4; // 两个系统中都单独存在的元素 {"shengzhen" "shanghai"}
string firstStr = first(citySet3); // beijing
string lastStr = last(citySet3); // shengzhen
string ele = next(nameSet1,"zhangsan",2); // 在集合中找zhangsan后面的第二个 集合中如果有重复元素会报错
string ele1 = item(nameSet1,1); // 找到索引值为1的元素
int ele2 = ord(nameSet1,"zhangsan"); // 找到zhangsan的索引值:0
execute{
writeln(unionset);
writeln(interset);
writeln(diffset);
writeln(symdiffset);
writeln(firstStr);
writeln(lastStr);
writeln(ele);
writeln(ele1);
writeln(ele2);
}
4.4.3.集合的顺序
集合可以是已排序状态、已排列状态或逆序状态。默认是已排序状态,默认是输入顺序。
可以使用sorted或reversed对集合排序。
元组集合如果有key,则根据每个key排序。如果没有key,则所有字段(有例外)参与排序。
ordered {string} orderedSet = {"shanghai","beijing","shengzhen","123cc","2frsd"}; // 已排序集合 默认情况 输入的顺序
sorted {string} sortedSet = {"shanghai","beijing","shengzhen","123cc","2frsd"}; // 已排列集合 元素按照自然升序(或降序)顺序排列。 字符串是词典序。
reversed {string} reversedSet = {"shanghai","beijing","shengzhen","123cc","2frsd"}; // 逆序排列集合
tuple Point {
key string name;
int x;
int y;
}
{Point} tupSet1 = {<"bcd",1,2>, <"abd",1,3>, <"abc",3,4>};
reversed {Point} tupSet2 = {<"bcd",1,2>, <"abd",1,3>, <"abc",3,4>}; // 逆序元组集合 比较元组的所有key,先比较第一个key然后比较第二个key。元组集合不使用键,那么会将整个元组(除了固定字段和数组字段)都考虑到排列操作中
sorted {Point} sortedTupSet2 = {<i,j,k> | <i,j,k> in tupSet1};
execute{
writeln(orderedSet); // {"shanghai" "beijing" "shengzhen" "123cc" "2frsd"}
writeln(sortedSet); // {"123cc" "2frsd" "beijing" "shanghai" "shengzhen"}
writeln(reversedSet);// {"shengzhen" "shanghai" "beijing" "2frsd" "123cc"}
writeln(tupSet1); // {<"bcd" 1 2> <"abd" 1 3> <"abc" 3 4>}
writeln(tupSet2); // {<"bcd" 1 2> <"abd" 1 3> <"abc" 3 4>}
writeln(sortedTupSet2);// {<"abc" 3 4> <"abd" 1 3> <"bcd" 1 2>}
}
4.5.通过数据结构解决稀疏性问题
就矩阵(或二维数组)而言,稀疏性指包含许多零值。
比如有100个仓库,给50个人发货,要算出这50个人的订单,应该最多只有50种仓库到用户的距离不为0,其余全为0;通过以下形式的声明可以利用稀疏性:
{string} Warehouses = ...;
{string} Customers = ...;
tuple Route {
string w;
string c;
}
{Route} routes = ...;
int transp[routes] = ... ;
该声明指定集 routes
,该集仅包含相关对 (warehouse, customer)。 然后,数组 transp
可以按此集编制索引,从而利用应用程序中存在的稀疏性。 很明显,对于大规模应用程序,这种方法会大幅降低内存消耗量,计算时间。
可以通过列出其值来初始化数组,就像目前为止提供的大部分示例一样。