使用.Net Core与Google Optimization Tools实现员工排班计划Scheduling

上一篇说完《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

将班次分配给护士

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

 星期日星期一星期二星期三星期四星期五星期六
护士A1011111
护士B0102222
护士C2220330
护士D3333003

 

.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 = 4;

        var num_shifts = 4;  // 班次数定为4,这样序号为0的班次表示是休息的班。

        var num_days = 7;


        // [START]

        // 创建班次变量

        var shifts = new Dictionary<(int, int), IntVar>();


        foreach (var j in Enumerable.Range(0, num_nurses))

        {

            foreach (var i in Enumerable.Range(0, num_days))

            {

                // shifts[(j, i)]表示护士j在第i天的班次,可能的班次的编号范围是:[0, num_shifts)

                shifts[(j, i)] = solver.MakeIntVar(0, num_shifts - 1, string.Format("shifts({0},{1})", j, i));

            }

        }


        // 将变量集合转成扁平化数组

        var shifts_flat = (from j in Enumerable.Range(0, num_nurses)

                           from i in Enumerable.Range(0, num_days)

                           select shifts[(j, i)]).ToArray();


        // 创建护士变量

        var nurses = new Dictionary<(int, int), IntVar>();


        foreach (var j in Enumerable.Range(0, num_shifts))

        {

            foreach (var i in Enumerable.Range(0, num_days))

            {

                // nurses[(j, i)]表示班次j在第i天的当班护士,可能的护士的编号范围是:[0, num_nurses)

                nurses[(j, i)] = solver.MakeIntVar(0, num_nurses - 1, 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(0, num_days))

        {

            var nurses_for_day = (from j in Enumerable.Range(0, num_shifts)

                                  select nurses[(j, day)]).ToArray();

            foreach (var j in Enumerable.Range(0, 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(0, num_days))

        {

            solver.Add((from j in Enumerable.Range(0, num_nurses)

                        select shifts[(j, i)]).ToArray().AllDifferent());

            solver.Add((from j in Enumerable.Range(0, 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(0, num_nurses))

        {

            solver.Add((from i in Enumerable.Range(0, num_days)

                        select shifts[(j, i)] > 0).ToArray().Sum() >= 5);

            solver.Add((from i in Enumerable.Range(0, num_days)

                        select shifts[(j, i)] > 0).ToArray().Sum() <= 6);

        }

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

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(0, num_nurses))

        {

            foreach (var j in Enumerable.Range(0, num_shifts))

            {

                works_shift[(i, j)] = solver.MakeBoolVar(string.Format("nurse%d shift%d", i, j));

            }

        }


        foreach (var i in Enumerable.Range(0, num_nurses))

        {

            foreach (var j in Enumerable.Range(0, num_shifts))

            {

                // 建立works_shift与shifts的关联关系

                // 一周内的值要么为0要么为1,所以Max定义的约束是最大值,恰好也是0或1,1表示至少在每周轮班一天

                solver.Add(works_shift[(i, j)] == (from k in Enumerable.Range(0, num_days)

                                                   select shifts[(i, k)].IsEqual(j)).ToArray().Max());

            }

        }


        // 对于每个编号不为0的shift, 满足至少每周最多同一个班次2个护士当班

        foreach (var j in Enumerable.Range(1, num_shifts - 1))

        {

            solver.Add((from i in Enumerable.Range(0, num_nurses)

                        select works_shift[(i, j)]).ToArray().Sum() <= 2);

        }

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

// 满足中班或晚班的护士前一天或后一天也是相同的班次

        // 用nurses的key中Tuple类型第1个item的值表示shift为2或3

        // shift为1表示早班班次,shift为0表示休息的班次

        solver.Add(solver.MakeMax(nurses[(2, 0)] == nurses[(2, 1)], nurses[(2, 1)] == nurses[(2, 2)]) == 1);

        solver.Add(solver.MakeMax(nurses[(2, 1)] == nurses[(2, 2)], nurses[(2, 2)] == nurses[(2, 3)]) == 1);

        solver.Add(solver.MakeMax(nurses[(2, 2)] == nurses[(2, 3)], nurses[(2, 3)] == nurses[(2, 4)]) == 1);

        solver.Add(solver.MakeMax(nurses[(2, 3)] == nurses[(2, 4)], nurses[(2, 4)] == nurses[(2, 5)]) == 1);

        solver.Add(solver.MakeMax(nurses[(2, 4)] == nurses[(2, 5)], nurses[(2, 5)] == nurses[(2, 6)]) == 1);

        solver.Add(solver.MakeMax(nurses[(2, 5)] == nurses[(2, 6)], nurses[(2, 6)] == nurses[(2, 0)]) == 1);

        solver.Add(solver.MakeMax(nurses[(2, 6)] == nurses[(2, 0)], nurses[(2, 0)] == nurses[(2, 1)]) == 1);


        solver.Add(solver.MakeMax(nurses[(3, 0)] == nurses[(3, 1)], nurses[(3, 1)] == nurses[(3, 2)]) == 1);

        solver.Add(solver.MakeMax(nurses[(3, 1)] == nurses[(3, 2)], nurses[(3, 2)] == nurses[(3, 3)]) == 1);

        solver.Add(solver.MakeMax(nurses[(3, 2)] == nurses[(3, 3)], nurses[(3, 3)] == nurses[(3, 4)]) == 1);

        solver.Add(solver.MakeMax(nurses[(3, 3)] == nurses[(3, 4)], nurses[(3, 4)] == nurses[(3, 5)]) == 1);

        solver.Add(solver.MakeMax(nurses[(3, 4)] == nurses[(3, 5)], nurses[(3, 5)] == nurses[(3, 6)]) == 1);

        solver.Add(solver.MakeMax(nurses[(3, 5)] == nurses[(3, 6)], nurses[(3, 6)] == nurses[(3, 0)]) == 1);

        solver.Add(solver.MakeMax(nurses[(3, 6)] == nurses[(3, 0)], nurses[(3, 0)] == nurses[(3, 1)]) == 1);

定义约束求解器的使用

// 将变量集合设置为求解的目标,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[] { 340, 2672, 7054 };


        foreach (var sol in a_few_solutions)

        {

            Console.WriteLine("Solution number {0}", sol);


            foreach (var i in Enumerable.Range(0, num_days))

            {

                Console.WriteLine("Day {0}", i);

                foreach (var j in Enumerable.Range(0, num_nurses))

                {

                    Console.WriteLine("Nurse {0} assigned to task {1}", j, collector.Value(sol, shifts[(j, i)]));

                }

                Console.WriteLine();

            }

        }

运行结果如下:

 

 

 

原文地址:http://www.cnblogs.com/BeanHsiang/p/8670378.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/321801.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

动态规划训练12 [G - You Are the One HDU - 4283 ]

2012天津区域赛的一道题目&#xff0c;题目链接如下 You Are the One HDU - 4283 这道题目要说思想的话其实并不是很难&#xff0c;但是我却没做出来。关键就在于读题读不懂&#xff08;How Vegetable I am&#xff01;&#xff09;&#xff0c;到最后搜了别人的题解才明白这道…

42、Java服务内存OOM原因分析

1、出现问题的可能原因 对于应用来说内存分配太少 对象创建太多&#xff0c;又没有释放&#xff0c;造成内存泄漏严重&#xff0c;导致内存耗尽 申请太多的系统资源&#xff0c;系统资源耗尽。例如&#xff1a;不断创建线程&#xff0c;不断发起网络连接 2、如何定位问题&a…

jzoj3850-Fibonacci进制【斐波那契倍增】

正题 题目大意:https://jzoj.net/senior/#main/show/3850 题目大意 定义f(i)f(i)f(i)表示第i1i1i1个斐波那契数 一个数转换成斐波那契进制后第iii位的0/10/10/1表示是否需要加上f(i)f(i)f(i)&#xff0c;然后将1∼∞1\sim \infty1∼∞转换成斐波那契进制后依次输出在屏幕上&a…

Slickflow.NET 开源工作流引擎基础介绍-.NET Core2.0 版本实现介绍

前言&#xff1a;.NET Core 是.NET Framework的新一代版本&#xff0c;是微软开发的第一个跨平台 (Windows、Mac OSX、Linux) 的应用程序开发框架&#xff08;Application Framework&#xff09;&#xff0c;未来也将会支持 FreeBSD 与 Alpine 平台。.Net Core也是微软在一开始…

动态规划训练13 [Catch That Cow poj3278]

Catch That Cow POJ - 3278 这道题我看大家用的方法都是bfs搜索&#xff0c;为什么在我看来这就是一个动态规划的题目啊啊啊啊啊啊啊 dp[x]表示从N出发到x所需要的最小时间 那么得到如下转移方程 如果x < N的话&#xff0c;那么只能通过走路来转移&#xff0c;所以dp[x] …

jzoj1246-挑剔的美食家【set,贪心】

正题 题目大意:https://jzoj.net/senior/#main/show/1246 题目大意 nnn头牛&#xff0c;第iii头吃的东西价格大于aia_iai​&#xff0c;鲜嫩度大于bib_ibi​。mmm个吃的&#xff0c;第iii个价格为cic_ici​&#xff0c;鲜嫩度为did_idi​。 求满足所有奶牛的情况下最少要花多少…

DotNetty 跨平台的网络通信库

久以来,.Net开发人员都非常羡慕Java有Netty这样&#xff0c;高效&#xff0c;稳定又易用的网络通信基础框架。终于微软的Azure团队&#xff0c;使用C#实现的Netty的版本发布。不但使用了C#和.Net平台的技术特点&#xff0c;并且保留了Netty原来绝大部分的编程接口。让我们在使用…

26、临时表的创建和重复数据的处理

UPDATE student b SET b.sname dd WHERE b.id (SELECT a.id FROM student a WHERE a.id 3) Mysql中根据条件&#xff08;表A中的字段&#xff09;操作表A中的数据时是不可以的 所以借助临时表来删除/更新重复的数据&#xff0c;原理就是删除每组重复数据中除id值最大的其他…

动态规划训练14 [Max Sum Plus Plus HDU - 1024 ]

Max Sum Plus Plus HDU - 1024 题意大致是说给你你个序列&#xff0c;把它划分成不相交的几个连续的部分&#xff0c;然后把这个几个部分求和&#xff0c;求出和的最大值。 我们定义子结构 dp[i][j] 表示的是从前j个元素&#xff0c;划分成i段所得的最大和。 则我们可以得到…

jzoj1247-队列变换【字符串hash,二分】

正题 题目链接:https://jzoj.net/senior/#main/show/1247 题目大意 一个长度为nnn的字符串&#xff0c;每次选择头或者尾加入新的字符串末端&#xff0c;求字典序最小的新的字符串。 解题思路 我们发现若剩下的字符串比翻转之后份字符串字典序大那么就加入头&#xff0c;否则…

1、java简介

关于java介绍也没什么好说的&#xff0c;在这里简单介绍一下&#xff0c;说起java&#xff0c;我第一想到的就是它的简单和强大&#xff0c;简单是简单易学&#xff0c;开发速度快&#xff1b;强大是其功能强大&#xff0c;各个领域都可使用&#xff0c;其代码一次编译可以处处…

C# 观察者模式 以及 delegate 和 event

观察者模式这里面综合了几本书的资料.需求有这么个项目: 需求是这样的:一个气象站, 有三个传感器(温度, 湿度, 气压), 有一个WeatherData对象, 它能从气象站获得这三个数据. 还有三种设备, 可以按要求展示气象站的最新数据.WeatherData的结构如下:有3个get方法, 分别获取最新的…

动态规划训练15 [Monkey and Banana HDU - 1069 ]

Monkey and Banana HDU - 1069 题意大致是将一个长方体通过旋转&#xff0c;使得摞起来的建筑最高。但是必须满足这么一个条件&#xff0c;那就是上面的长方体的底面一定要完全被下一个长方体的底面完全覆盖&#xff0c;并且要有空位&#xff0c;就像楼梯那样。 由于每一个长方…

牛客-无形的博弈【结论题,快速幂】

正题 题目链接:https://ac.nowcoder.com/acm/contest/1104/A 题目大意 一个010101序列&#xff0c;如果首项是000&#xff0c;那么你就可以变111或者不变。如果是111那么对方可以选择变000或者不变&#xff0c;如果全变成0那么你获胜&#xff0c;如果永远不能全变成0那么对手…

2、JAVA开发环境的搭建

上次说到java应用之所以做到跨平台&#xff0c;是因为其依赖于java虚拟机&#xff0c;java想要运行需要依赖于特定的运行环境&#xff0c;称为JRE&#xff0c;如果想要开发java应用&#xff0c;则需要用到开发工具包&#xff0c;也就是JDK&#xff0c;所以这里就要说一下这几者…

Serilog Tutorial

在过去的几年中&#xff0c;结构化日志已经大受欢迎。而Serilog是 .NET 中最著名的结构化日志类库 ,我们提供了这份的精简指南来帮助你快速了解并运用它。0. 内容设定目标认识Serilog事件和级别触发和收集结构化数据为过滤和关联添加事件标记大海捞针 [Finding needles in the …

动态规划训练16 [Doing Homework HDU - 1074 ]

Doing Homework HDU - 1074 这是一道状态压缩DP&#xff08;从N < 15就可以看出来&#xff09;。 我们定义二进制状态S代表的是目前已经安排好的任务 dp[S].val代表的是目前已经安排好的任务的扣分的最小值 dp[S].sumT代表的是目前已经安排好的任务所需要的时间 状态转移…

3、java中的数据类型和运算符

数据类型 数据类型是对数据存储在内存中位置的一种抽象表示&#xff0c;java的数据类型总体上分为两大类&#xff1a;基本数据类型和引用数据类型。 1、基本数据类型 基本数据类型是语言本身定义的&#xff0c;数据结构上有说基本数据类型表示的是真实的数字和字符&#xff0…

牛客-十二桥问题【最短路,状压dp】

正题 题目链接:https://ac.nowcoder.com/acm/contest/1104/B 题目大意 nnn个点mmm条边的无向图&#xff0c;kkk条必须经过的边&#xff0c;求从1出发经过这kkk条边再回到1的最短路。 解题思路 我们每条边两段的端点和1是我们需要用到的特征点&#xff0c;我们用计算出每个特征…

动态规划训练17 [Super Jumping! Jumping! Jumping! HDU - 1087 ]

Super Jumping! Jumping! Jumping! HDU - 1087 过于简单懒得说了 #include <cstdio> #include <algorithm> #include <cstring> using namespace std; const int MAX 1000; int a[MAX]; int dp[MAX]; main(){int N;while(scanf("%d",&N) ! E…