上一篇说完《Google Optimization Tools介绍》,让大家初步了解了Google Optimization Tools是一款约束求解(CP)的高效套件。那么我们用.NET Core与Google Optimization Tools来实现一个有关员工排班计划的场景感受一下。

众所周知,现实生活中有些工作是7X24工作制的,如呼叫中心或医院护士,最常见的问题就是如何安排多名员工进行倒班,制定好日程时间表,使每班配备足够的人员来维持运营。时间表有各种不同的约束要求,例如:员工不允许连续两次轮班之类。接下来我们介绍类似问题的一个示例,叫护士调度问题,并展示了如何使用.NET Core与Google Optimization Tools实现排班计划。

护士调度问题

在本例中,医院主管需要为四名护士创建一个周时间表,具体情况如下:

  • 每天分为早、中、晚三班轮班。
  • 在每一天,所有护士都被分配到不同的班次,除了有一名护士可以休息。
  • 每位护士每周工作五到六天。
  • 每个班次不会有超过两名护士在工作。
  • 如果一名护士某一天的班次是中班或晚班,她也必须在前一日或次日安排相同的班次。

有两种方式来描述我们需要解决的问题:

  • 指派护士轮班
  • 将班次分配给护士

事实证明,解决问题的最好方法是结合两种方式来求解。

指派护士轮班

下表显示了指派护士轮班视角的排班情况,这些护士被标记为A,B,C,D,换班,编号为0 - 3(其中0表示护士当天不工作)。

 

星期日

星期一 星期二 星期三 星期四 星期五 星期六
班次1

A

B

A

A

A

A

A

班次2

C

C

C

B

B

B

B

班次3

D

D

D

D

C

C

D

将班次分配给护士

下表显示了将班次分配给护士视角的排班情况。

  星期日 星期一 星期二 星期三 星期四 星期五 星期六
护士A 1 0 1 1 1 1 1
护士B 0 1 0 2 2 2 2
护士C 2 2 2 0 3 3 0
护士D 3 3 3 3 0 0 3

.NET Core解决方案

首先使用VS017创建一个.NET Core的控制台项目。

由于Google Optimization Tools对.NET Core的支持还不友好,需要通过NuGet引用一个第三方专门为Core编译好的程序集以及相关依赖,Google.OrTools.Core和CrossPlatformLibraryLoader。

准备完成后,我们逐一介绍编码的过程。首先介绍几个基本概念:

  • IntVar是约束求解中使用最多的变量形式,一般约束问题中变化的对象都应该定义为一个类似在一定范围内整形数值的变量。
  • solver.MakeIntVar是创建约束求解中变量的方法,约束求解一定会定义一些可变化的对象,一般都需要转化成数值类型。
  • solver.Add是添加若干约束条件的方法。
  • solver.MakePhase定义了求解的目标以及求解的取值策略。
  • solver.Solve进行求解,并对指定的集合赋值。
  • solver.MakeAllSolutionCollector表示获取解的集合对象。

定义约束求解器和相关变量

我们用shift和nurse分别来表示班次和护士 。

       // 创建约束求解器.
var solver = new Solver("schedule_shifts");
var num_nurses = ;
var num_shifts = ; // 班次数定为4,这样序号为0的班次表示是休息的班。
var num_days = ; // [START]
// 创建班次变量
var shifts = new Dictionary<(int, int), IntVar>(); foreach (var j in Enumerable.Range(, num_nurses))
{
foreach (var i in Enumerable.Range(, num_days))
{
// shifts[(j, i)]表示护士j在第i天的班次,可能的班次的编号范围是:[0, num_shifts)
shifts[(j, i)] = solver.MakeIntVar(, num_shifts - , string.Format("shifts({0},{1})", j, i));
}
} // 将变量集合转成扁平化数组
var shifts_flat = (from j in Enumerable.Range(, num_nurses)
from i in Enumerable.Range(, num_days)
select shifts[(j, i)]).ToArray(); // 创建护士变量
var nurses = new Dictionary<(int, int), IntVar>(); foreach (var j in Enumerable.Range(, num_shifts))
{
foreach (var i in Enumerable.Range(, num_days))
{
// nurses[(j, i)]表示班次j在第i天的当班护士,可能的护士的编号范围是:[0, num_nurses)
nurses[(j, i)] = solver.MakeIntVar(, num_nurses - , string.Format("shift{0} day{1}", j, i));
}
}

shifts和nurses两个对象含义如下:

shifts[(j, i)]表示护士j在第i天的班次,可能的班次的编号范围是:[0, num_shifts)。
nurses[(j, i)]表示班次j在第i天的当班护士,可能的护士的编号范围是:[0, num_nurses)。
shifts_flat是将shifts的Values简单地处理成扁平化,后面直接用于当参数传给约束求解器solver以指定需要求解的变量。

定义shifts和nurses的对应关系

将每一天的nurses单独列出来,按照编号顺序扁平化成一个数组对象,s.IndexOf(nurses_for_day)是一种OR-Tools要求的特定用法,相当于nurses_for_day[s]求值。这里利用了s的值恰好是在nurses_for_day中对应nurse的编号。注意这里的两层foreach循环,v外层不能互换,必须是现在这样,内层循环的主体对象与shifts_flat一致。

       // 定义shifts和nurses之前的关联关系
foreach (var day in Enumerable.Range(, num_days))
{
var nurses_for_day = (from j in Enumerable.Range(, num_shifts)
select nurses[(j, day)]).ToArray();
foreach (var j in Enumerable.Range(, num_nurses))
{
var s = shifts[(j, day)];
// s.IndexOf(nurses_for_day)相当于nurses_for_day[s]
// 这里利用了s的值恰好是在nurses_for_day中对应nurse的编号
solver.Add(s.IndexOf(nurses_for_day) == j);
}
}

定义护士在不同的班次当班约束

AllDifferent方法是OR-Tools定义约束的方法之一,表示指定的IntVar数组在进行计算时受唯一性制约。满足每一天的当班护士不重复,即每一天的班次不会出现重复的护士的约束条件,同样每一个护士每天不可能同时轮值不同的班次。

        // 满足每一天的当班护士不重复,每一天的班次不会出现重复的护士的约束条件
// 同样每一个护士每天不可能同时轮值不同的班次
foreach (var i in Enumerable.Range(, num_days))
{
solver.Add((from j in Enumerable.Range(, num_nurses)
select shifts[(j, i)]).ToArray().AllDifferent());
solver.Add((from j in Enumerable.Range(, num_shifts)
select nurses[(j, i)]).ToArray().AllDifferent());
}

定义护士每周当班次数的约束

Sum方法是OR-Tools定义运算的方法之一。注意shifts[(j, i)] > 0运算被重载过,其返回类型是WrappedConstraint而不是默认的bool。满足每个护士在一周范围内只出现[5, 6]次。

        // 满足每个护士在一周范围内只出现[5, 6]次
foreach (var j in Enumerable.Range(, num_nurses))
{
solver.Add((from i in Enumerable.Range(, num_days)
select shifts[(j, i)] > ).ToArray().Sum() >= );
solver.Add((from i in Enumerable.Range(, num_days)
select shifts[(j, i)] > ).ToArray().Sum() <= );
}

定义每个班次在一周内当班护士人数的约束

Max方法是OR-Tools定义运算的方法之一,表示对指定的IntVar数组求最大值。注意MakeBoolVar方法返回类型是IntVar而不是默认的bool,works_shift[(i, j)]为True表示护士i在班次j一周内至少要有1次,BoolVar类型的变量最终取值是0或1,同样也表示了False或True。满足每个班次一周内不会有超过两名护士当班工作。

        // 创建一个工作的变量,works_shift[(i, j)]为True表示护士i在班次j一周内至少要有1次
// BoolVar类型的变量最终取值是0或1,同样也表示了False或True
var works_shift = new Dictionary<(int, int), IntVar>(); foreach (var i in Enumerable.Range(, num_nurses))
{
foreach (var j in Enumerable.Range(, num_shifts))
{
works_shift[(i, j)] = solver.MakeBoolVar(string.Format("nurse%d shift%d", i, j));
}
} foreach (var i in Enumerable.Range(, num_nurses))
{
foreach (var j in Enumerable.Range(, num_shifts))
{
// 建立works_shift与shifts的关联关系
// 一周内的值要么为0要么为1,所以Max定义的约束是最大值,恰好也是0或1,1表示至少在每周轮班一天
solver.Add(works_shift[(i, j)] == (from k in Enumerable.Range(, num_days)
select shifts[(i, k)].IsEqual(j)).ToArray().Max());
}
} // 对于每个编号不为0的shift, 满足至少每周最多同一个班次2个护士当班
foreach (var j in Enumerable.Range(, num_shifts - ))
{
solver.Add((from i in Enumerable.Range(, num_nurses)
select works_shift[(i, j)]).ToArray().Sum() <= );
}

定义护士在中班和晚班的连班约束

        // 满足中班或晚班的护士前一天或后一天也是相同的班次
// 用nurses的key中Tuple类型第1个item的值表示shift为2或3
// shift为1表示早班班次,shift为0表示休息的班次
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == ); solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );

定义约束求解器的使用

        // 将变量集合设置为求解的目标,Solver有一系列的枚举值,可以指定求解的选择策略。
var db = solver.MakePhase(shifts_flat, Solver.CHOOSE_FIRST_UNBOUND, Solver.ASSIGN_MIN_VALUE); // 创建求解的对象
var solution = solver.MakeAssignment();
solution.Add(shifts_flat);
var collector = solver.MakeAllSolutionCollector(solution);

执行求解计算并显示结果

        solver.Solve(db, new[] { collector });
Console.WriteLine("Solutions found: {0}", collector.SolutionCount());
Console.WriteLine("Time: {0}ms", solver.WallTime());
Console.WriteLine(); // 显示一些随机的结果
var a_few_solutions = new[] { , , }; foreach (var sol in a_few_solutions)
{
Console.WriteLine("Solution number {0}", sol); foreach (var i in Enumerable.Range(, num_days))
{
Console.WriteLine("Day {0}", i);
foreach (var j in Enumerable.Range(, num_nurses))
{
Console.WriteLine("Nurse {0} assigned to task {1}", j, collector.Value(sol, shifts[(j, i)]));
}
Console.WriteLine();
}
}

运行结果如下:

最后,放出完整的代码如下

using Google.OrTools.ConstraintSolver;
using System;
using System.Collections.Generic;
using System.Linq; public class ConsoleApp1
{
static void Main()
{
// 创建约束求解器.
var solver = new Solver("schedule_shifts");
var num_nurses = ;
var num_shifts = ; // 班次数定为4,这样序号为0的班次表示是休息的班。
var num_days = ; // [START]
// 创建班次变量
var shifts = new Dictionary<(int, int), IntVar>(); foreach (var j in Enumerable.Range(, num_nurses))
{
foreach (var i in Enumerable.Range(, num_days))
{
// shifts[(j, i)]表示护士j在第i天的班次,可能的班次的编号范围是:[0, num_shifts)
shifts[(j, i)] = solver.MakeIntVar(, num_shifts - , string.Format("shifts({0},{1})", j, i));
}
} // 将变量集合转成扁平化数组
var shifts_flat = (from j in Enumerable.Range(, num_nurses)
from i in Enumerable.Range(, num_days)
select shifts[(j, i)]).ToArray(); // 创建护士变量
var nurses = new Dictionary<(int, int), IntVar>(); foreach (var j in Enumerable.Range(, num_shifts))
{
foreach (var i in Enumerable.Range(, num_days))
{
// nurses[(j, i)]表示班次j在第i天的当班护士,可能的护士的编号范围是:[0, num_nurses)
nurses[(j, i)] = solver.MakeIntVar(, num_nurses - , string.Format("shift{0} day{1}", j, i));
}
} // 定义shifts和nurses之前的关联关系
foreach (var day in Enumerable.Range(, num_days))
{
var nurses_for_day = (from j in Enumerable.Range(, num_shifts)
select nurses[(j, day)]).ToArray();
foreach (var j in Enumerable.Range(, num_nurses))
{
var s = shifts[(j, day)];
// s.IndexOf(nurses_for_day)相当于nurses_for_day[s]
// 这里利用了s的值恰好是在nurses_for_day中对应nurse的编号
solver.Add(s.IndexOf(nurses_for_day) == j);
}
} // 满足每一天的当班护士不重复,每一天的班次不会出现重复的护士的约束条件
// 同样每一个护士每天不可能同时轮值不同的班次
foreach (var i in Enumerable.Range(, num_days))
{
solver.Add((from j in Enumerable.Range(, num_nurses)
select shifts[(j, i)]).ToArray().AllDifferent());
solver.Add((from j in Enumerable.Range(, num_shifts)
select nurses[(j, i)]).ToArray().AllDifferent());
} // 满足每个护士在一周范围内只出现[5, 6]次
foreach (var j in Enumerable.Range(, num_nurses))
{
solver.Add((from i in Enumerable.Range(, num_days)
select shifts[(j, i)] > ).ToArray().Sum() >= );
solver.Add((from i in Enumerable.Range(, num_days)
select shifts[(j, i)] > ).ToArray().Sum() <= );
} // 创建一个工作的变量,works_shift[(i, j)]为True表示护士i在班次j一周内至少要有1次
// BoolVar类型的变量最终取值是0或1,同样也表示了False或True
var works_shift = new Dictionary<(int, int), IntVar>(); foreach (var i in Enumerable.Range(, num_nurses))
{
foreach (var j in Enumerable.Range(, num_shifts))
{
works_shift[(i, j)] = solver.MakeBoolVar(string.Format("nurse%d shift%d", i, j));
}
} foreach (var i in Enumerable.Range(, num_nurses))
{
foreach (var j in Enumerable.Range(, num_shifts))
{
// 建立works_shift与shifts的关联关系
// 一周内的值要么为0要么为1,所以Max定义的约束是最大值,恰好也是0或1,1表示至少在每周轮班一天
solver.Add(works_shift[(i, j)] == (from k in Enumerable.Range(, num_days)
select shifts[(i, k)].IsEqual(j)).ToArray().Max());
}
} // 对于每个编号不为0的shift, 满足至少每周最多同一个班次2个护士当班
foreach (var j in Enumerable.Range(, num_shifts - ))
{
solver.Add((from i in Enumerable.Range(, num_nurses)
select works_shift[(i, j)]).ToArray().Sum() <= );
} // 满足中班或晚班的护士前一天或后一天也是相同的班次
// 用nurses的key中Tuple类型第1个item的值表示shift为2或3
// shift为1表示早班班次,shift为0表示休息的班次
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == ); solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == );
solver.Add(solver.MakeMax(nurses[(, )] == nurses[(, )], nurses[(, )] == nurses[(, )]) == ); // 将变量集合设置为求解的目标,Solver有一系列的枚举值,可以指定求解的选择策略。
var db = solver.MakePhase(shifts_flat, Solver.CHOOSE_FIRST_UNBOUND, Solver.ASSIGN_MIN_VALUE); // 创建求解的对象
var solution = solver.MakeAssignment();
solution.Add(shifts_flat);
var collector = solver.MakeAllSolutionCollector(solution); solver.Solve(db, new[] { collector });
Console.WriteLine("Solutions found: {0}", collector.SolutionCount());
Console.WriteLine("Time: {0}ms", solver.WallTime());
Console.WriteLine(); // 显示一些随机的结果
var a_few_solutions = new[] { , , }; foreach (var sol in a_few_solutions)
{
Console.WriteLine("Solution number {0}", sol); foreach (var i in Enumerable.Range(, num_days))
{
Console.WriteLine("Day {0}", i);
foreach (var j in Enumerable.Range(, num_nurses))
{
Console.WriteLine("Nurse {0} assigned to task {1}", j, collector.Value(sol, shifts[(j, i)]));
}
Console.WriteLine();
}
}
}
}

使用.NET Core与Google Optimization Tools实现员工排班计划Scheduling的更多相关文章

  1. Google Optimization Tools实现员工排班计划Scheduling【Python版】

    上一篇介绍了<使用.Net Core与Google Optimization Tools实现员工排班计划Scheduling>,这次将Google官方文档python实现的版本的完整源码献 ...

  2. 使用.NET Core与Google Optimization Tools实现加工车间任务规划

    前一篇文章<使用.NET Core与Google Optimization Tools实现员工排班计划Scheduling>算是一种针对内容的规划,而针对时间顺序任务规划,加工车间的工活儿 ...

  3. Google Optimization Tools实现加工车间任务规划【Python版】

    上一篇介绍了<使用.NET Core与Google Optimization Tools实现加工车间任务规划>,这次将Google官方文档python实现的版本的完整源码献出来,以满足喜爱 ...

  4. Google Optimization Tools介绍

    Google Optimization Tools(OR-Tools)是一款专门快速而便携地解决组合优化问题的套件.它包含了: 约束编程求解器. 简单而统一的接口,用于多种线性规划和混合整数规划求解, ...

  5. Google PageSpeed Tools 性能测试分析

    今天给大家介绍下一个工具:Google PageSpeed Tools,根据官方的介绍,简单梳理如下: Page Speed Insights能针对移动设备和电脑设备衡量网页的性能.该工具会抓取网址两 ...

  6. ASP.NET Core 使用 Google 验证码(reCAPTCHA v3)代替传统验证码

    写在前面 友情提示: Google reCAPTCHA(v3下同) 的使用不需要"梯子",但申请账号的时候需要! Google reCAPTCHA 的使用不需要"梯子&q ...

  7. 在Ubuntu环境下配置NIMH MEG Core Facility之CTF Tools

    在Ubuntu环境下配置NIMH MEG Core Facility之CTF Tools 网站有提示: The install script won't work, but you can copy ...

  8. NET Core 2.1 Global Tools

    微软工程师Nate McMaster的博文.NET Core 2.1 Global Tools https://natemcmaster.com/blog/2018/05/12/dotnet-glob ...

  9. Google performance Tools (gperftools) 使用心得

    Google performance Tools (gperftools) 使用心得 gperftools是google开发的一款非常实用的工具集,主要包括:性能优异的malloc free内存分配器 ...

随机推荐

  1. java通过sftp对linux服务器文件夹进行操作

    本文主要讲sftp对linux服务器的文件和文件夹进行操作,windows server 服务器不支持. package com.lx.ftp; import java.io.File; import ...

  2. 【转】Javascript基本类型和引用类型的区别

    根据[转贴]进一步补充 今天明白了一个困扰很久的问题:引用类型和基本类型的区别与联系要明白这个问题,首先需要理解堆栈的概念.那什么又是堆栈,有什么区别和联系呢?堆:首先堆是动态分配的,JVM并不会自动 ...

  3. 利用WCF搭建RESTful--纯代码启动

    最近学习了这几年忽略了的当前几乎所有的开发技术,有深有浅,而服务层最有兴趣的是RESTfull,看的是java的书.因为不熟悉JSP,于是找了本书细细研读了一次. dotnet的实现也相对简单,网上也 ...

  4. python advanced programming (Ⅲ)

    IO编程 IO在计算机中指Input/Output.由于程序和运行时数据是在内存中驻留,由CPU来执行,涉及到数据交换的地方,通常是磁盘.网络等,就需要IO接口. IO编程中,Stream(流)是一个 ...

  5. C++ 中的异常机制分析

    C++异常机制概述 异常处理是C++的一项语言机制,用于在程序中处理异常事件.异常事件在C++中表示为异常对象.异常事件发生时,程序使用throw关键字抛出异常表达式,抛出点称为异常出现点,由操作系统 ...

  6. curl工具介绍和常用命令

    curl是利用URL语法在命令行方式下工作的开源文件传输工具.它被广泛应用在Unix.Linux发行版中,并且有DOS和Win32.Win64的移植版本.curl是一个利用URL规则在命令行下工作的文 ...

  7. hdu 2191 【背包问题】

    题目 请输出能够购买大米的最多重量,注意是重量不是价值. 把每一种物品拧出来,用01背包解决. #include <cstdio> #include <iostream> #i ...

  8. java锁的种类以及辨析(转载)

    java锁的种类以及辨析(一):自旋锁 锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) .这些已经写好提供的锁为我 ...

  9. codeforce 489d bfs分层处理

    这个题确实没想到用bfs进行分层处理,后来看到了大佬的题解之后才想到了这一点 bfs dfs早就学了,可是还是不大会应用到这上面 可以分为三层,起始点,中间点,尾点,需要的数据是中间点到尾点的访问次数 ...

  10. 防Xss注入

    转自博客:https://blog.csdn.net/qq_21956483/article/details/54377947 1.什么是XSS攻击 XSS又称为CSS(Cross SiteScrip ...