迟到的总结-By Glede
队友连昭鹏的总结:http://www.cnblogs.com/lzplzp/archive/2012/10/22/2732946.html
我们一开始交流的时候,就决定基本模仿生活中的电梯运行过程来设计程序,生活中常见的电梯的算法应当是比较成熟地经过了各种考验的优秀算法,至少能把所有人运到终点,然后再可以根据我们的程序结果进行一些优化。但是一开始讨论论的时候我们就遇到了各种问题,光是电梯的运行逻辑处理的情况非常多,多台电梯的运行细节更多,没有办法整理出一个特别清晰的算法,程序编写十分困难。所以后来我们决定分开两个层次对电梯调度进行抽象:
1,是对于关注于单个电梯的运行任务。要保存电梯有关的信息并且记录它的所有请求,包括上下行的Direction Request和Destination Request,运行任务保证分配到一台电梯的请求能够在有限时间内被完成。
2,是对于管理多个电梯的上层调度系统,对于收到的一个请求,调度系统将请求分配给最合适的电梯去处理它,并在分配时能认为该电梯一定能处理它。这个调度系统保证所有的请求能够被分配到至少一台电梯。
这样的调度程序设计中存在两个类,Scheduler和ElevatorTask,分职清晰,两个类要处理的任务逻辑上独立不会相互交叉,降低了编写程序的复杂性。只要两个类都能完成自己的设计目标,就可以保证将所有乘客送到,而使得运送平均时间更短,需要一些更复杂的分析。
历史版本简介:
第一个可以完成所有任务的Scheduler版本。
MyScheduler
Version: 0.20
Codename: Auto Bus
算法简介:
ElevatorTask使用了一个含有三个状态Idle、Pick、Drop的状态机。
Idle状态会检查自己是否拥有未完成的任务,根据任务的请求方向(DirectionRequest要求的)和运行的方向(响应该请求需要前往一个楼层,这个楼层相对电梯当前楼层的方向),选择进入Pick或者Drop状态;
Pick状态是逆行的,比如向下行驶去响应一个上行的请求,到达请求楼层之后将会更改自己的运行方向然后进入Drop状态响应即将到来的DestinationRequest;
Drop状态是顺行的,行驶方向和请求方向是相同的,运行时每次寻找在运行方向上最近的请求楼层停止,然后再继续寻找下一个需要停止的最近楼层,如果找不到,就返回Idle状态。
电梯停止时,认为以当前楼层为目的地的Destination请求已经完成,认为请求方向和当前运行方向相同的DirectionReqest已经完成。如果超重该楼层的乘客会再次按键发出请求,所有不用担心。
超重处理,认为电梯剩余空间少于120时便为超重,这样保证了电梯不会在一个有乘客因超重未能上电梯的楼层当机进入死循环。
Scheduler的逻辑相对简单:
收到DirectionRequest,选择一个估计到达时间最快且未超重的电梯,将请求分配给它并且移除这个请求;如果找不到(比如所有电梯都在逆着行,都会返回-1),在下一个时间点再次搜寻;
收到DestinationRequest,直接将请求分配给所请求的电梯然后移除。
估计时间使用了非常宽松的算法,考虑电梯的运行方向,电梯运行方向,如果不经过则返回-1表示不能到达;否则返回根据相距楼层估计的时间,不考虑已知的中间停留时间。
效率:
在3个官方用例上与 Naive Bus 比较
Naive Bus Auto Bus
Passenger1 307.05 62.5
Passenger2 1024.403 327.276
Passenger3 1424.563 259.59
version 0.20 Auto Bus :第一个可以完成所有任务的Scheduler版本。
算法简介:
ElevatorTask使用了一个含有三个状态Idle、Pick、Drop的状态机。
Idle状态会检查自己是否拥有未完成的任务,根据任务的请求方向(DirectionRequest要求的)和运行的方向(响应该请求需要前往一个楼层,这个楼层相对电梯当前楼层的方向),和一个比较靠谱的逻辑优先方案,选择一个目标楼层进入Pick或者Drop状态;如果找不到,则继续保持Idle状态;
Pick状态是逆行的,比如向下行驶去响应一个上行的请求,到达请求楼层之后将会更改自己的运行方向然后进入Drop状态响应即将到来的DestinationRequest;
Drop状态是顺行的,行驶方向和请求方向是相同的,运行时每次寻找在运行方向上最近的请求楼层停止,然后再继续寻找下一个需要停止的最近楼层,如果找不到,就返回Idle状态。
电梯停止时,认为以当前楼层为目的地的Destination请求已经完成,认为请求方向和当前运行方向相同的DirectionReqest已经完成。如果超重该楼层的乘客会再次按键发出请求,所有不用担心。
超重处理,认为电梯剩余空间少于120时便为超重,这样保证了电梯不会在一个有乘客因超重未能上电梯的楼层当机进入死循环。
Scheduler的逻辑相对简单:
收到DirectionRequest,选择一个估计到达时间最快且未超重的电梯,将请求分配给它并且移除这个请求;如果找不到(比如所有电梯都在逆着行,都会返回-1),在下一个时间点再次搜寻,我们将这称为Hurry Bus调度算法,第一次在尝试编写的 version 0.11 的Hurry Bus中使用(由于状态机出现BUG未能跑到终点);
收到DestinationRequest,直接将请求分配给所请求的电梯然后移除。
估计时间使用了非常宽松的算法,考虑电梯的运行方向,电梯运行方向,如果不经过则返回-1表示不能到达;否则返回根据相距楼层估计的时间,不考虑已知的中间停留时间。
效率:
以下的历史版本简介摘自历史源代码注释,并且使用仿照官方分布生成的40个测试用例进行统计测试。
simple是少量人情况,random是大量人来自于随机楼层的情况,ComeToWork是上班高峰,LeaveWork是下班高峰。
根据第一个版本的成绩我们分析认为,当有多个请求同时发生在一个Idle状态电梯上时,根据Auto Bus的跳转方法,是从0开始遍历至21层找到其中第一个有请求的楼层作为目标楼层,这使得Auto Bus对于第三个测试用例即LeaveWork情况的应对不是很理想,这是一个失误,因为之间没有考虑到很多个请求在一个tick中并发,因此我们首先在跳转方法上进行改进,此外是对电梯的估算到达时间方法进行了改进,考虑了已知的中途停靠,使估算的时间更准确(从统计上对更距离更远的电梯进行远距离调度的可能性更大,是有利于大量人员的运载的),发放了一个测试性的历史版本。
第二个可以跑到终点的测试性的历史版本
/** * MyScheduler * Version: 0.21 * Codename: Auto Bus X * * 算法简介: * 调度算法依然是简单的和 Hurry Bus 相同的算法,Scheduler 每次寻找能 * 够最快响应要求楼层的电梯跑向预定楼层,但是修改了估计时间的算法使之 * 更精确。ElevatorTask 使用的状态机基本和 Auto Bus 相同,具有自主寻 * 地完成所有任务的能力,对某一些状态转换做了一些优化。 * * 改进: * 1.更精细的时间估计方法。Auto Bus x 估计电梯响应时间时计入已知的中 * 间停靠楼层所额外花费的时间。 * 2.修改了 Elevator Task 从空闲状态进入接乘时,确定启动方向的方法。 * 现在 Elevator Task 在同一时间接收到多个 Pick 呼叫时,会自动分析上 * 下行到开始释放乘客的时间,选择时间短的方向前进。 * * 效率: * 相对与 Auto Bus,平均时间改进略有进步,在下班高峰情况时的成绩提升 * 很多,但是同时在上班高峰情况时成绩有一定的下降。 * * 生成的40个测试用例统计平均值 * (1)Auto Bus * (2)Auto Bus X * * (1) (2) * Simple: 118.36 102.67 * Random: 150.7434 145.4367 * ComeToWork: 340.393 457.7388 * LeaveWork: 250.3113 194.9353 */
改进的效果可以看到,统计值上能够体现Simple Random和LeaveWork情况成绩都有提高,但是ComeToWork下降了,这也使合乎情理的,Auto Bus 的跳转方法更有利于上楼人群多的情况。对于这一点,两种跳转方法互斥但是各有利弊,我们想到了可以给01层的请求优先级来控制跳转,并且测试了几个固定对0、1层优先、而对2~21层继续使用找最快释放乘客时间的跳转。结果是ComeToWork的成绩大幅提高,但是不能避免LeaveWork的成绩下降。经过多次测试无法中和,我们决定不再做出妥协,使用通过检测来自01层的用户请求数来切换优先级状态,使两个算法能够在不同情况下发挥自己的长处。编程时,检测需要考虑的开关时间控制和检测区间记录花了一些力气,因为要保证没有误判(一旦误判发生,对Random的成绩影响可以认为是线性的、对LeaveToWork的成绩的影响则是毁灭性的),优先模式启动的越快对ComeToWork越有利,但是误判的可能性也越高。最后我们得到了一个比较合适的检测区间是100ticks,阈值是 100ticks*0.15 req/tick,至短持续时间是50ticks。
自动切换优先状态的稳定历史版本,参考注释:
/** * MyScheduler * Version: 0.22 * Codename: Auto Bus Shift * * 算法简介: * 继续沿用自 Hurry Bus 以来的最快响应调度算法,进一步改进的具有自主寻 * 地完成任务能力的状态机。同时革新的引入了上班高峰检测、优先响应机制。 * * 改进: * 1.再次改进了状态机从 Idle 状态进入 Pick 状态的检测条件和优先顺序, * 使平均时间更短、表现更稳定。 * 2.上班高峰检测机制,关注记录当前0、1层上楼请求的频率,当其超过阈值时 * 将开启上班高峰优先模式,并且至少会持续一段时间。当0、1层上楼请求的频 * 率连续小于阈值一定时间之后将关闭上班优先模式。 * 上班高峰优先模式提供来自0、1层上楼请求的两种优先策略,半优先模式优先 * 将0、1层上层请求分配给所有电梯,但是电梯的状态机从空闲状态进入工作时 * 候并不强制处理它;全优先模式则是强制有限处理。实际的测试中我们发现全 * 优先模式能够取得更好的综合成绩,因此默认使用全优先模式。 * * 效率: * 相对与 Auto Bus X,两种模式应对 ComeToWork 的成绩有大幅提高,其他模 * 式的平均时间差别不大。 * * 生成的40个测试用例统计平均值 * (1)Auto Bus X * (2)Auto Bus Shift * (3)Auto Bus Shift (Half Priority) * * (1) (2) (3) * Simple: 102.67 116.8 105.085 * Random: 145.4367 168.9481 141.9443 * ComeToWork: 457.7388 302.4932 355.0303 * LeaveWork: 194.9353 196.3478 196.3478 */
即使使用了全优先模式的Auto Bus Shift,ComeToWork成绩也只能说是勉强可以接受,而且Simple和Random成绩也降低了一些。我们并不满意,还是决定继续改进。首先还是针对ComeToWork情况。我们经过思考认为还是对电梯承载能力未充分利用,而最快的算法应该能够对电梯的空间进行更大程度的利用,即电梯在运行过程中的 FreeCapcaity 积 ticks 应当是最小的。我们的调度程序中有一个非常浪费FreeCapacity的地方,就是在Pick状态中,电梯会直接走到最远的逆行请求楼层,中途不再接受其他上人请求。于是我们针对Auto Bus状态机这个特点进行如下优化,事实上这也更接近现代电梯调度的朴素算法了。
另一点,对于一个电梯已经收到分配的DirectionRequest,是有可能被其他电梯完成的,比如其他电梯在超重状态未收到分配,但是在请求的楼层,超重的电梯有人到达目的地,停止并且腾出了空间,完成了收到DirectionRequest分配电梯的任务,那么这个电梯就可能就可以中途少停一次了,于是另一个细节的优化就是让电梯任务之间进行交互,通知已经完成的任务并且将它们取消(事实上这个情况出现的还挺多的,我们测试时记录了电梯自己任务被其他电梯完成而取消的次数,总是能够超过我们估计的次数)。
一次经历了大部分修改的并且多次测试,选取的最优、最稳定版,已经是最终版本的雏形。统计成绩很优秀……
/** * MyScheduler * Version: 0.3 * Codename: Auto Bus RR * * 算法简介: * 继续沿用自 Hurry Bus 以来的最快响应调度算法,使用只有Idle和Drop两个 * 状态的精简状态机,开启全优先模式的上班高峰检测、优先响应机制。新增收 * 回请求机制。 * 代号 RR 代表 Reduced(精简) 和 Recall-Supported(支持收回请求)。 * * 改进: * 1.精简了状态机的工作状态,为了最大限度利用所有电梯的剩余空间,不再使 * 用不能在中途上人的 Pick 状态,现在电梯在任何时候都能够响应需要中途停 * 靠上人的请求。 * 2.现在一半的电梯在空闲模式下接受多个请求时候会优先选择上行,另一半优 * 先选择下行。 * 3.收回请求机制,电梯在完成楼层的 Pick 请求后通知 Scheduler 和其他电 * 梯,其他电梯自动消除该请求。CancelledReq 属性记录了收到其他电梯的完 * 成通知而移除请求的次数。 * 4.大量的测试表明宽松的时间估计方法(只关注相距楼层而不计入中途停靠) * 在统计和个例中都能比较精细的时间估计方法有更好的表现,因此 0.3 版本 * 的时间估计再次回到 0.20 版本中使用的宽松方法。 * * 效率: * 相对与 Auto Bus Shift,三种模式成绩均有显著提高,尤其ComeToWork模式 * 的成绩已经进入较好范围内。 * * 生成的40个测试用例统计平均值 * (1)Auto Bus X * (2)Auto Bus Shift * (3)Auto Bus Shift (Half Priority) * (4)Auto Bus RR * * (1) (2) (3) (4) * Simple: 102.67 116.8 105.085 97.18999 * Random: 145.4367 168.9481 141.9443 142.2705 * ComeToWork: 457.7388 302.4932 355.0303 258.1164 * LeaveWork: 194.9353 196.3478 196.3478 182.3004 */
顺便放上我们测试时的包括各种未提交版本的数据(tcq是另一组同学的成绩):
最后一个发放版本是在受到其他组的提醒之后做出的一个修正,算法上没有任何改动,只是修改了检测01请求的方法,使其不会在测试用例以外的情况发生更多的误判,也因此测试用例的成绩并没有变化。
/** * MyScheduler * Version: 0.31 * Codename: Auto Bus RR * * 算法简介: * 继续沿用自 Hurry Bus 以来的最快响应调度算法,使用只有Idle和Drop两个 * 状态的精简状态机,开启全优先模式的上班高峰检测、优先响应机制。新增收 * 回请求机制。 * 代号 RR 代表 Reduced(精简) 和 Recall-Supported(支持收回请求)。 * * 改进: * 修改了检测上班高峰的方法,现在上班高峰需要满足请求比例超过阈值和请求 * 频率超过阈值两个条件。 * * 效率: * 与 Auto Bus RR 相当。 */
最终版本三个官方用例的截图:
passenger1
passenger2
passenger3
至此我们的电梯之旅告一段落,我和连昭鹏完成了一次不错的合作,结对编程的好处貌似是可以分担一些工作,但是在两个人的工程水平不一的情况下,代码工作量没有得到分担,甚至还可能出现一些问题,幸好这个工程不是很大。
连昭鹏的优点很多,算法细节思考的很到位,能够做测试,善于交流
缺点也比较明显,写代码比较慢。虽然我完成了大部分的编码工作,但是他在一开始讨论算法时的建议和观察到的细节给了我们的结果很大的帮助。
这是我们一起工作时的照片(嗯,代码是我……)
==========================华丽分割线=====================================
附:官方给出测试用例的分布规律和我们使用的用例生成程序。
关于测试数据
人数都是1000
来的时间均匀分布于0~3600(官方给出的说明和图像一致,就不放图了)
两份数据的平均体重,75.43kg和 75.631kg(而不是官方声明的60kg)
看图片:
从图上看可以认为是以 70kg 分界的两段平均分布,45-70为一段,70~120一段,人数都是500人。这样计算下来理论值是75.75kg很接近
关于楼层:
psng3
来自,是2~20的均匀分布,去是 0和1 90%均匀分布,2~20 10%的均匀分布,注意没有人from 0 1 层的。
这是来自的分布图
psng2 是类似的情况,from 是 80% 0~1,20% 2~20 均匀分布
to 是2~20的均匀分布。也没有人要去 0 1 层。
这样可以自己随机一坨测试数据了……