优化建模之MiniZinc(四) 复杂逻辑约束和调度问题实例

笔者对MiniZinc的学习多有赖于Coursera上的modelling for discrete modeling》系列以及MiniZinc的tutorial,如果对离散优化建模有兴趣,花些时间去上墨尔本大学和香港中文大学的这一系列课程必然能有所裨益。

约束的本质

实际上在求解器中,约束就是一系列的True or False。无论多么复杂的约束,本质上也就是布尔表达式。

既然明了约束的本质,那么复杂逻辑约束也无非就是通过逻辑联结词串联的一系列简单逻辑约束的组合。

值得一提的是,通常在求解器中,会用整数01来替代FalseTrue,通过MiniZinc中内置的bool2int函数,我们可以将布尔表达式显式转化为整数。

逻辑联结词(Logical Connectives)

在数理逻辑中,常用的联结词有5个,下面一一介绍在MiniZinc中的对应形式

  • 合取(Conjunction):MiniZinc中用/\表示,a /\ b用自然语言描述就是 a and b
  • 析取(Disjunction):MiniZinc中用\/表示,a \/ b用自然语言描述就是a or b
  • 等价(Biconditional):MiniZinc中用<->表示,a <-> b 用自然语言表示就是a if and only if b
  • 蕴含(Material implication):MiniZinc中用->表示,a -> b用自然语言表示就是if a then b
  • 否定(Negation):MiniZinc中用not表示,not a意义就等同于自然语言表示了

求解器中对复杂逻辑约束的处理

各个求解器中有不同的处理复杂逻辑约束的技术,但是总体来说,求解器对于非合取约束会使用一种称为具体化的奇数。

对于非合取约束(否定式、析取式),求解器会为该约束附上一个布尔变量,使得:

  • 如果该约束满足,那么变量取true
  • 如果约束不满足,那么变量取false

例如constraint x>y \/ not p(x,y);会被求解器拆分为:

constraint b1 <-> x>y; constraint b2 <-> p(x,y); constraint b1 \/ not b2;

了解这一点,可以帮助我们明了不同的约束表达效率的高下。

低效的约束表达

约束优化的求解器很多都是基于”约束满足“技术来实现求解的,它们一般接受一组变量以及一组处于合取关系的约束,并尝试返回解。

其中一些类型的约束是求解器处理起来效率比较低的:

  • 否定式not x = y:求解器比较难以处理否定形式,因此我们通常希望把它替换为取反形式x != y
  • 析取式c1 \/ c2:尽可能尝试化简或者在可能的情况下用合取式来替换
  • bool2int
  • 蕴含式c1->c2:最好令c1特别简单
  • 存在exists([...]):效率比较低,尽可能不要使用,在循环操作中forall要比exists高效的多

调度问题

调度问题通常指一系列任务需要分配到一些资源上,满足某些约束,并且优化一个特定的目标函数。经典的调度问题有车间生产调度、计算机中任务的调度等等。

涉及的概念:

  • 任务:每个任务都具有开始时间,持续时间,其他属性。

array[TASK] of var int: start;

array[TASK] of var int: duration;

  • 优先次序:某个任务要先于另一个任务完成。这在调度问题中是非常常见的一类约束,例如一个零件必须要先进行热处理才能进行机加工,例如洗车时要先完成清洗才能打蜡,等等。

start[t1] + duration[t1] <= start[t2]

资源受限项目调度问题(Resource constrained project scheduling problem, RCPSP)

资源受限项目调度问题是调度问题中非常经典的一类。在这类调度问题中,我们的每个资源都有若干个副本,也就是说在同一时间任意一种资源都有有限的数量,能用于满足各种工作需求。在实际场景中,比如我们对应每种机加工的机床都有若干台,或者我们任务调度时,有若干个CPU核心可供使用,或者我们在每家医院中,都有给定数量的病床等。

对资源占用的建模

解决RCPSP的主要难点在于怎么对多个资源副本的占用情况进行建模:

  • 我们假设任务t使用res[t]份资源
  • 假设在任何时刻,对某种资源的占用都有个最大限度L

那么我们可以顺着时间或者任务的思路,对这个约束进行建模:

  1. 基于时间的分解:在每个时间点i,资源的使用都少于给定的限度L

forall(i in TIME)(sum(t in TASK)((start[t] <= i /\ start[t] + duration[t] > i) * res[t]) <= L);

这种分解存在一些问题:

  • 约束数目为card(TIME)*card(TASK),规模太大,通常来说主要问题在于我们的card(TIME)可能会很大
  • 当时间区间为浮点数时,区间的分割方式就并不那么显而易见了
  1. 基于任务的分解:注意到资源使用超量的情况只会发生在某个任务开始时,因此我们只需要在任务的开始时间检查资源的使用情况即可:

forall(t2 in TASK)(sum(t in TASK)((start[t] <= start[t2] /\ start[t] + duration[t] > start[t2]) * res[t]) <= L);

当然,考虑到 t 和 t2应该是不等的,上面的表述还可以进行一定优化:

forall(t2 in TASK)(sum(t in TASK where t != t2)((start[t] <= start[t2] /\ start[t] + duration[t] > start[t2]) * res[t]) + res[t2] <= L);

此时的约束数目为card(TASK)^2,在通常情况下要比基于时间的分解少很多。

  1. MiniZinc的全局约束cumulative

cumulative(<开始时间数组>, <持续时间数组>, <资源使用数组>, <资源使用限度>)

保证任务进行时任何时间点资源的使用量都不超过限度

注意cumulative中开始时间数组可以不是确切的值,也可以以一个范围的形式给出,如cumulative([0..3, 0..3, 2..3, 0..4], [4,3,2,3],[2,1,2,3],4)

实例与模型求解

我们可以来看一个例子,假设我们在处理多CPU下的任务调度,我们有3个处理器和7个任务,每个任务的用时分别为2, 14, 4, 16, 6, 5, 3个单位时间。同时任务有一定的先后顺序要求,任务1要先于任务4完成,任务3要先于任务7完成。求一个总用时最少的任务分配方案。

这里我们用第二种方式,也就是基于任务的分解对问题进行建模,得到如下模型文件4_1_JobScheduling.mzn

% 分离调度问题

%--------------------------
% 数据部分
enum TASK; % 所有工作的枚举类型
enum RESOURCE; % 所有资源的枚举类型,在这个问题中,使我们的CPU
int: n_task; % 工作的数量
int: n_resource; % 资源的数量
int: n_pred; % 优先次序关系的数量
int: max_time = 100; % 估算一个最大用时
array[TASK] of int: duration; % 完成每个任务需要的时长
array[1..n_pred, 1..2] of TASK: precedence; % 要求的优先次序


%--------------------------
% 决策变量
array[TASK] of var 0..max_time: start;

%--------------------------
% 约束
% 1. 由于我们的资源副本之间没有区别,并不需要额外区分放在哪个CPU中执行,因此要求每个时间点的资源使用数 < 给定资源数量即可
constraint forall(t2 in TASK)(sum(t in TASK where t != t2)
    (start[t] <= start[t2] /\ start[t] + duration[t] > start[t2]) <= n_resource - 1);
% 2. 保证任务的先后次序
constraint forall(i in 1..n_pred)
    (start[precedence[i, 1]] + duration[precedence[i,1]] <= start[precedence[i, 2]]);

%--------------------------
% 目标函数
var 0..max_time: makespan = max([start[t] + duration[t] | t in TASK]);
solve minimize makespan;

%--------------------------
% 输出
output ["Starting time of jobs: \(start)\nMinimal time span: \(makespan)"];

建立数据文件4_1_JobScheduling.dzn

TASK = {t1, t2, t3, t4, t5, t6, t7}; % 所有工作
RESOURCE = {c1, c2, c3}; % 所有资源
n_task = 7; % 工作的数量
n_resource = 3; % 资源的数量
n_pred = 2; % 优先次序关系的数量
duration = [2, 14, 4, 16, 6, 5, 3];
precedence = [| t1, t4
              | t3, t7|]; % 任务的优先次序

求解之后得到答案:

Starting time of jobs: [0, 1, 5, 2, 9, 0, 15]
Minimal time span: 18
----------
==========

可视化之后,可以得到下面的一个类甘特图,可以看到我们的任务优先次序也得到了满足:

4_1 JobScheduling
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容