更多复杂模型 =================== 在上一节中,我们介绍了MiniZinc模型的基本结构。在这一节中,我们介绍数组和集合数据结构,枚举类型,以及更加复杂的约束。 .. _sec-arrayset: 数组和集合 --------------- 在绝大多数情况下,我们都是有兴趣建一个约束和变量的个数依赖于输入数据的模型。 为了达到此目的,我们通常会使用 :index:`数组 ` 。 考虑一个关于金属矩形板温度的简单有限元素模型。通过把矩形板在2维的矩阵上分成有限个的元素, 我们近似计算矩形板上的温度。 一个模型在 :numref:`ex-laplace` 中给出。它声明了有限元素模型的宽 ``w`` 和高 ``h`` 。 声明 .. literalinclude:: examples/laplace.mzn :language: minizinc :lines: 5-9 声明了四个固定的整型集合来描述有限元素模型的尺寸: ``HEIGHT`` 是整个模型的整体高度,而 ``CHEIGHT`` 是省略了顶部和底部的中心高度, ``WIDTH`` 是模型的整体宽度,而 ``CWIDTH`` 是省略了左侧和右侧的中心宽度。最后,声明了一个浮点型变量组成的行编号从 :math:`0` 到 :math:`w` ,列编号从 :math:`0` 到 :math:`h` 的两维数组 ``t`` 用来表示金属板上每一点的温度。 我们可以用表达式 :mzn:`t[i,j]` 来得到数组中第 :math:`i^{th}` 行和第 :math:`j^{th}` 列的元素。 拉普拉斯方程规定当金属板达到一个稳定状态时,每一个内部点的温度是它的正交相邻点的平均值。约束 .. literalinclude:: examples/laplace.mzn :language: minizinc :lines: 16-18 保证了每一个内部点 :math:`(i,j)` 是它的四个正交相邻点的平均值。 约束 .. literalinclude:: examples/laplace.mzn :language: minizinc :lines: 20-24 限制了每一个边的温度必须是相等的,并且给了这些温度名字:``left`` , ``right`` , ``top`` 和 ``bottom``。 而约束 .. literalinclude:: examples/laplace.mzn :language: minizinc :lines: 26-30 确保了角的温度(这些是不相干的)被设置为0.0。 我们可以用 :numref:`ex-laplace` 中给出的模型来决定一个被分成5 :math:`\times` 5个元素的金属板的温度。其中左右下侧的温度为0,上侧的温度为100。 .. literalinclude:: examples/laplace.mzn :language: minizinc :name: ex-laplace :caption: 决定稳定状态温度的有限元平板模型 (:download:`laplace.mzn `). 运行命令 .. code-block:: bash $ minizinc --solver cbc laplace.mzn 得到输出 .. code-block:: none 0.00 100.00 100.00 100.00 0.00 0.00 42.86 52.68 42.86 0.00 0.00 18.75 25.00 18.75 0.00 0.00 7.14 9.82 7.14 0.00 0.00 0.00 0.00 0.00 0.00 ---------- .. defblock:: 集合 .. index:: single: set 集合变量用以下方式声明 .. code-block:: minizincdef set of <类型-实例化> : <变量名> ; 整型,枚举型(参见后面),浮点型和布尔型集合都可以定义。 决策变量集合只可以是类型为整型或者枚举型的变量集合。 集合常量有以下形式 .. code-block:: minizincdef { <表达式-1>, ..., <表达式-n> } 或者是以下形式的整型,枚举型或浮点型 :index:`范围` 表达式 .. code-block:: minizincdef <表达式-1> .. <表达式-2> 标准的 :index:`集合操作符 ` 有:元素属于 (:mzn:`in`), (非严格的) 集合包含 (:mzn:`subset`), (非严格的) 超集关系 (:mzn:`superset`), 并集 (:mzn:`union`), 交集 (:mzn:`intersect`), 集合差运算 (:mzn:`diff`), 集合对称差 (:mzn:`symdiff`) 和集合元素的个数 (:mzn:`card`). 我们已经看到集合变量和集合常量(包含范围)可以被用来作为变量声明时的隐式类型。 在这种情况下变量拥有集合元素中的类型并且被隐式地约束为集合中的一个成员。 我们的烤蛋糕问题是一个非常简单的批量生产计划问题例子。在这类问题中,我们希望 去决定每种类型的产品要制造多少来最大化利润。同时制造一个产品会消耗不同数量固定的资源。 我们可以扩展 :numref:`ex-cakes2` 中的MiniZinc模型为一个不限制资源和产品类型的模型去处理这种类型的问题。 这个模型在 :numref:`ex-prod-planning` 中给出。一个(烤蛋糕问题的)数据文件例子在 :numref:`fig-prod-planning-data` 中给出。 .. literalinclude:: examples/simple-prod-planning.mzn :language: minizinc :name: ex-prod-planning :caption: 简单批量生产计划模型 (:download:`simple-prod-planning.mzn `). .. literalinclude:: examples/simple-prod-planning-data.dzn :language: minizinc :name: fig-prod-planning-data :caption: 简单批量生产计划模型的数据文件例子 (:download:`simple-prod-planning-data.dzn `). 这个模型的新特征是只用枚举类型 :index:`枚举类型 ` 。 这使得我们可以把资源和产品的选择作为模型的参数。 模型的第一个项 .. code-block:: minizinc enum Products; 声明 ``Products`` 为 *未知的* 产品集合。 .. defblock:: 枚举类型 .. index:: single: enumerated type single enum 枚举类型,我们称为 ``enums`` , 用以下方式声明 .. code-block:: minizincdef enum <变量名> ; 一个枚举类型用以下赋值的方式定义 .. code-block:: minizincdef enum <变量名> = { <变量名-1>, ..., <变量名-n> } ; 其中 :mzndef:`<变量名-1>`, ..., :mzndef:`<变量名-n>` 是名为 :mzndef:`<变量名>` 的枚举类型中的元素。 通过这个定义,枚举类型中的每个元素也被有效地声明为这个类型的一个新的常量。 声明和定义可以像往常一样结合为一行。 第二个项声明了一个整型数组: .. code-block:: minizinc array[Products] of int: profit; ``profit`` 数组的 :index:`下标集合 ` 是 ``Products`` 。理想情况下,这种声明方式表明只有集合 ``Products`` 中的元素才能被用来做数组的下标。 有 :math:`n` 个元素组成的枚举类型中的元素的行为方式和整数 :math:`1\dots n` 的行为方式很像。它们可以被比较,它们可以按照它们出现在枚举类型定义中的顺序被排序,它们可以遍历,它们可以作为数组的下标,实际上,它们可以出现在一个整数可以出现的任何地方。 在数据文件例子中,我们用一列整数来初始化数组 .. code-block:: minizinc Products = { BananaCake, ChocolateCake }; profit = [400,450]; 意思是香蕉蛋糕的利润是400,而巧克力蛋糕的利润是450。 在内部, ``BananaCake`` 会被看成是像整数1一样,而 ``ChocolateCake`` 会被看成像整数2一样。MiniZinc虽然不提供明确的列表类型,但用 :mzn:`1..n` 为下标集合的一维数组表现起来就像列表。我们有时候也会称它们为列表 :index:`lists ` 。 根据同样的方法,接下来的两项中我们声明了一个资源集合 ``Resources`` , 一个表明每种资源可获得量的数组 ``capacity``。 更有趣的是项 .. code-block:: minizinc array[Products, Resources] of int: consumption; 声明了一个两维数组 ``consumption`` 。 :mzn:`consumption[p,r]` 的值是制造一单位的产品 :mzn:`p` 所需要的资源 :mzn:`r` 的数量。其中第一个下标是行下标,而第二个下标是列下标。 数据文件包含了一个两维数组的初始化例子: .. code-block:: minizinc consumption= [| 250, 2, 75, 100, 0, | 200, 0, 150, 150, 75 |]; 注意分隔符 ``|`` 是怎样被用来分隔行的。 .. defblock:: 数组 .. index: single: array 因此,MiniZinc提供一维和多维数组。它们用以下类型来声明: .. code-block:: minizincdef array [ <下标集合-1>, ..., <下标集合-n> ] of <类型-实例化> MiniZinc要求数组声明要给出每一维的下标集合。下标集合或者是一个整型范围,一个 被初始化为整型范围的集合变量,或者是一个 :index:`枚举类型 ` 。 数组可以是所有的基类型:整型,枚举型,布尔型,浮点型或者字符串型。 这些可以是固定的或者不固定的,除了字符串型,它只可以是参数。数组也可以作用于 集合但是不可以作用于数组。 :index:`一维数组常量 ` 有以下格式 .. code-block:: minizincdef [ <表达式-1>, ..., <表达式-n> ] 而 :index:`二维数组常量 ` 有以下格式 .. code-block:: minizincdef [| <表达式-1-1>, ..., <表达式-1-n> | ... | <表达式-m-1>, ..., <表达式-m-n> |] 其中这个数组有 ``m`` 行 ``n`` 列。 内建函数 :mzn:`array1d` , :mzn:`array2d` 等家族可以被用来从一个列表(或者更准确的说是一个一维数组)去实例化任何维度的数组。 调用 .. code-block:: minizincdef arrayd(<下标集合-1>, ..., <下标集合-n>, <列表>) 返回一个 ``n`` 维的数组,它的下标集合在前 ``n`` 个参数给出,最后一个参数包含了数组的元素。 例如 :mzn:`array2d(1..3, 1..2, [1, 2, 3, 4, 5, 6])` 和 :mzn:`[|1, 2 |3, 4 |5, 6|]` 是相等的。 数组元素按照通常的方式获取 :index:`获取 ` : :mzn:`a[i,j]` 给出第 :math:`i^{th}` 行第 :math:`j^{th}` 列的元素。 .. \pjs{New array functions!} 串联操作符 ``++`` 可以被用来串联两个一维的数组。 结果得到一个列表,即一个元素从1索引的一维数组。 例如 :mzn:`[4000, 6] ++ [2000, 500, 500]` 求得 :mzn:`[4000, 6, 2000, 500, 500]` 。内建函数 :mzn:`length` 返回一维数组的长度。 模型的下一项定义了参数 :mzn:`mproducts` 。它被设为可以生产出的任何类型产品的数量上限。 这个确实是一个复杂的内嵌数组推导式和聚合操作符例子。在我们试图理解这些项和剩下的模型之前,我们应该先介绍一下它们。 首先,MiniZinc提供了在很多函数式编程语言都提供的列表推导式。例如,列表推导式 :mzn:`[i + j | i, j in 1..3 where j < i]` 算得 :mzn:`[2 + 1, 3 + 1, 3 + 2]` 等同于 :mzn:`[3, 4, 5]` 。 :mzn:`[3, 4, 5]` 只是一个下标集合为 :mzn:`1..3` 的数组。 MiniZinc同时也提供了集合推导式,它有类似的语法:例如 :mzn:`{i + j | i, j in 1..3 where j < i}` 计算得到集合 :mzn:`{3, 4, 5}` 。 .. defblock:: 列表和集合推导式 .. index: single: comprehension single: comprehension; list 列表推导式的一般格式是 .. code-block:: minizincdef [ <表达式> | <生成元表达式> ] :mzndef:`<表达式>` 指明了如何从 :mzndef:`<生成元表达式>` 产生的元素输出列表中创建元素。 生成元 :mzndef:`` 由逗号分开的一列生成元表达式组成,选择性地跟着一个布尔型表达式。 两种格式是 .. code-block:: minizincdef <生成元>, ..., <生成元> <生成元>, ..., <生成元> where <布尔表达式> 第二种格式中的可选择的 :mzndef:`<布尔型表达式>` 被用作生成元表达式的过滤器:只有满足布尔型表达式的输出列表中的元素才被用来构建元素。 :index:`生成元 ` :mzndef:`` 有以下格式 .. code-block:: minizincdef <标识符>, ..., <标识符> in <数组表达式> 每一个标识符是一个 *迭代器* ,轮流从数值表达式中取值,最后一个标识符变化的最迅速。 列表推导式的生成元和 :mzndef:`<布尔型表达式>` 通常不会涉及决策变量。如果它们确实涉及了决策变量,那么产生的列表是一列 :mzndef:`var opt ` ,其中$T$是 :mzndef:`<表达式>` 的类型。更多细节,请参考 :ref:`sec-optiontypes` 中有关选项类型 :index:`option types