ElevatorSimulation:电梯调度算法仿真与离散事件建模实践

发布时间:2026/10/29 0:23:49
ElevatorSimulation:电梯调度算法仿真与离散事件建模实践 简介ElevatorSimulation 是一套基于 Java 的摩天大楼电梯系统仿真项目重点探讨单向、双向、观光、高速、消防等电梯类型并实现先来先服务、最近楼层优先、最少行程时间、预调度及群控等多种调度算法。项目面向具备一定 Java 基础的开发者适合用于理解多电梯协调、呼叫请求建模与算法性能对比。压缩包共 75 个文件以 25 个 Java 源码文件为主体涵盖电梯实体、呼叫对象、流量生成、调度器及大楼管理核心模块并辅以 XML 配置、prefs 偏好设置、txt 说明等总大小约 70KB目录层级清晰便于按类阅读。已有 402 人学习浏览。通过模拟流量输入与不同调度策略的替换读者可直观观察各算法在等待时间和运行效率上的差别其中 Selective Collective、SearchBased 等改进策略对理解电梯群控优化尤具参考价值。项目代码还涉及面向对象接口设计、数据结构与多种算法实践既能作为课程设计参考也可作为 Java 进阶项目练习帮助提升大型仿真系统的拆分与编码能力。1. ElevatorSimulation电梯调度算法仿真的那点事在《模拟城市》里规划过摩天楼的人十有八九都被电梯流量卡过脖子。现实中高层建筑电梯系统更是个黑匣子——楼层一多单梯运力不够双梯又要考虑并联调度早高峰打卡的人堆在厅门电梯来回空跑调度算法稍微笨一点等梯时间就肉眼可见地涨。这个叫 ElevatorSimulation 的 Java 项目就是把这套复杂系统掰开揉碎的仿真平台它能模拟单梯、并联梯群、双层轿厢电梯内置 FIFO、SSTF、SCAN、LOOK 以及带方向优先级的分层调度策略通过离散事件仿真把每一趟运行拆成到达、开门、上下客、关门、移动这几个事件输出平均等待时间、平均乘梯时间、系统吞吐量。适合正在做调度算法毕业设计的学生或者想给楼宇控制系统选梯控策略的集成工程师。2. 先把仿真模型拆开离散事件驱动下的电梯物理与乘客逻辑2.1 为什么电梯仿真必须用离散事件而不是时间片轮询很多入门者第一反应是开一个 while(true) 循环每秒更新一次所有电梯的位置。这个思路在 20 层以下的单体建筑里勉强能跑但双梯并联、楼层上百的时候时间片轮询会产生两个问题一是仿真精度受限于时间步长步长设小了性能崩设大了事件被平均化——比如开门动作只需要 2.8 秒你按 1 秒步长去刷新误差就被放大了二是乘客到达是一个连续随机过程时间片刷新根本表达不了第 42.7 秒来一个人这种精确时刻。ElevatorSimulation 的底层是离散事件队列Discrete Event Queue每个事件带一个时间戳事件按时间排序仿真时钟直接跳到下一个事件发生时刻而不是逐秒推进。这意味着开门、关门、移动、乘客到达这些动作都作为独立事件插入队列。以某次 40 层双梯仿真为例事件总数在 3 万次左右如果换成 100ms 步长轮询光空转检查就是 40 万次性能差一个量级。2.2 电梯物理模型从额定速度到加减速曲线电梯模型不是简单的位置 速度 × 时间真实电梯有加速度限制和加减速距离。ElevatorSimulation 中每台电梯配置如下参数public class ElevatorConfig { private int capacity 12; // 额定载客量人 private double maxSpeed 3.5; // 额定速度m/s private double acceleration 1.2; // 加速度m/s² private double deceleration 1.2; // 减速度m/s² private double doorOpenTime 2.8; // 开门时间s private double doorCloseTime 2.2; // 关门时间s private double floorHeight 3.2; // 层高m private int[] servedFloors; // 服务楼层集合用于分区模式 }参数说明capacity直接影响捎带算法能否在上下客高峰继续响应新事件maxSpeed和acceleration共同决定了电梯从静止加速到匀速再减速停靠的行程时间。常见做法是计算单层运行时间时先判断是否达到最大速度——如果楼层间距小于加速距离加上减速距离电梯根本跑不到额定速度就得减速这叫短行程限速。仿真里如果没有短行程判断就会出现 30 层电梯跑 1 层和跑 20 层耗时一样的不合理结果。public double travelTime(int startFloor, int endFloor) { double distance Math.abs(endFloor - startFloor) * floorHeight; double accelDist maxSpeed * maxSpeed / (2 * acceleration); // 加速段距离 double totalAccelDecelDist 2 * accelDist; if (distance totalAccelDecelDist) { // 短行程纯加减速到达不了最大速度 return 2 * Math.sqrt(distance / acceleration); } else { // 长行程加速 匀速 减速 double cruiseDist distance - totalAccelDecelDist; return maxSpeed / acceleration // 加速时间 cruiseDist / maxSpeed // 匀速时间 maxSpeed / deceleration; // 减速时间 } }逻辑说明这段代码先算出加速到额定速度所需的距离accelDist如果楼层间距小于两倍这个距离就按纯加减速运动学公式算时间否则拆成三段——加速、匀速、减速。这才是电梯调度仿真里最常见的短行程陷阱不处理这段调度对比的结果会失真。2.3 乘客到达模型泊松到达和上下行比例乘客到达不是均匀分布的。ElevatorSimulation 里提供了两种到达模式均匀到达用于测试系统稳定态泊松到达用于模拟早晚高峰的突发流量。泊松到达的关键参数是到达率 λ人/分钟相邻两个乘客的到达间隔服从指数分布。public Passenger generatePassenger() { Random random new Random(); // 指数分布采样平均到达间隔为 1/lambda 分钟 double interval -Math.log(1 - random.nextDouble()) / lambda; int currentFloor random.nextInt(totalFloors); int destFloor; do { destFloor random.nextInt(totalFloors); } while (destFloor currentFloor); boolean upward destFloor currentFloor; return new Passenger(currentFloor, destFloor, now interval, upward); }参数说明nextDouble()取 [0,1) 均匀分布-log(1-x)把均匀分布变换成指数分布这是蒙特卡洛仿真的标准做法。do-while用来防止生成同层乘客——真实场景中没人会从 12 层进电梯又按 12 层。now interval是乘客到达事件的绝对时间戳它会被插入离散事件队列。3. 从零搭一个 40 层双梯仿真跑通 FIFO 和 SSTF3.1 定义楼宇结构和服务基础参数拿到 ElevatorSimulation 源码后第一件事不是跑测试而是打开SimulationConfig.java定义你的实验楼宇。以 40 层摩天楼为例层高 3.2 米电梯数量 2 台载重 1350kg约 18 人额定速度 4m/s。仿真时长设 60 分钟前面 5 分钟作为预热期不计入统计——这个预热很重要否则开局电梯都在一楼傻等前几分钟数据会把平均等待时间拉高。SimulationConfig config new SimulationConfig.Builder() .floors(40) // 总楼层数不含地下层 .floorHeight(3.2) // 层高单位米 .elevatorCount(2) // 电梯数量 .capacity(18) // 单梯载客 18 人 .maxSpeed(4.0) // 额定速度 4m/s .lambda(3.0) // 泊松到达率平均每分钟 3 人 .simulationMinutes(60) // 仿真时长 60 分钟 .warmupMinutes(5) // 预热 5 分钟 .strategy(SSTF) // 先跑 SSTF 最短寻道时间优先 .build();3.2 跑通 FIFO 基线先知道最笨的调度长什么样FIFOFirst In First Out是电梯调度里最简单的策略——按乘客产生的先后顺序逐一向电梯派单。电梯当前没有任务就停在该楼层来新乘客就按顺序走。这个策略实现起来只有几十行核心逻辑public class FifoScheduler implements ElevatorScheduler { private QueuePassenger requestQueue new LinkedList(); Override public void onPassengerArrive(Passenger p, ListElevator elevators) { // FIFO 策略不判断方向直接进入全局队列 requestQueue.offer(p); dispatch(elevators); } private void dispatch(ListElevator elevators) { // 选择当前最空闲的电梯任务数最少 Elevator target elevators.stream() .min(Comparator.comparingInt(e - e.getPendingTasks().size())) .orElse(null); if (target ! null !requestQueue.isEmpty()) { Passenger next requestQueue.poll(); target.addTask(new Task(next.getFromFloor(), next.getToFloor())); } } }逻辑说明FIFO 的核心数据结构就是一个先入先出队列onPassengerArrive在乘客到达事件被处理时触发dispatch从队列头部取一个任务分配给任务数最少的电梯。这里没有做任何方向合并和路径优化是标准的每响应一个人就派一趟模型。跑完 FIFO 基线后你会看到 40 层双梯下平均等待时间在 90 秒以上峰值可能到 150 秒。这个数字很重要——它是后续所有调度策略的对比基准没有这个基线后面 SCAN 提升多少百分比就无从谈起。3.3 SSTF 实现贪心离当前层最近的请求SSTFShortest Seek Time First从磁盘调度算法搬过来核心思想是每次选离电梯当前层最近的乘客请求。这段代码能跑通但千万别急着下结论——SSTF 有个致命缺陷下面避坑章节细说。public class SstfScheduler implements ElevatorScheduler { Override public void onPassengerArrive(Passenger p, ListElevator elevators) { // 谁离得近谁接单不考虑方向 Elevator best elevators.stream() .min(Comparator.comparingInt(e - Math.abs(e.getCurrentFloor() - p.getFromFloor()))) .orElse(null); if (best ! null) { best.addTask(new Task(p.getFromFloor(), p.getToFloor())); } } }逻辑说明SSTF 用Math.abs(e.getCurrentFloor() - p.getFromFloor())计算电梯当前楼层到乘客所在楼层距离选距离最小者。每来一个乘客事件就重新计算一次所以它是一种全局贪心。跑完 SSTF 你会发现等待时间比 FIFO 低不少但有一个严重问题——电梯会频繁掉头。4. 方向优先与分层调度SCAN/LOOK 在高楼里的真正形态4.1 SCAN 电梯算法把一栋楼变成一条磁盘磁道SCAN 算法也叫电梯算法电梯保持在一个方向上运行直到该方向前方没有请求才掉头。相比 SSTF 的频繁掉头SCAN 更像真实电梯——同一方向上顺路的请求可以被捎带处理。ElevatorSimulation 的 SCAN 实现维护每台电梯的运行状态上行/下行/空闲当乘客按下按钮请求按方向进入上行集合或下行集合。public class ScanScheduler implements ElevatorScheduler { private static final int UP 1; private static final int DOWN -1; private final MapInteger, Integer directionMap new HashMap(); Override public void onPassengerArrive(Passenger p, ListElevator elevators) { Elevator e selectElevator(p, elevators); // 乘客请求按方向分组 if (p.isUpward()) { e.addUpRequest(p.getFromFloor(), p.getToFloor()); } else { e.addDownRequest(p.getFromFloor(), p.getToFloor()); } } private Elevator selectElevator(Passenger p, ListElevator elevators) { // 选一个当前运动方向和乘客请求方向最匹配的电梯 return elevators.stream() .filter(e - e.getDirection() 0 || e.getDirection() directionMap.get(p.getFromFloor())) .min(Comparator.comparingInt(e - Math.abs(e.getCurrentFloor() - p.getFromFloor()))) .orElseGet(() - elevators.get(0)); } }逻辑说明directionMap记录了每层楼当前存在的请求方向selectElevator优先选中方向一致或空闲的电梯避免把上行的乘客派给正在下行的电梯。运行中电梯会扫描当前方向上的所有请求到达最高请求楼层后掉头。SCAN 的关键参数是是否在响应完当前方向请求后立即掉头真实电梯一般会加一小段空闲等待时间避免刚掉头又来一个反向请求。4.2 LOOK 变体没有请求就掉头减少空跑LOOK 是 SCAN 的改良版——SCAN 不管前方有没有请求都跑到楼顶再掉头LOOK 只跑到当前方向最后一个请求楼层就掉头。在高峰时段 SCAN 和 LOOK 差别不大但平峰期 LOOK 能省掉大量空跑行程。ElevatorSimulation 里 LOOK 的判断核心如下public boolean shouldReverse(Elevator elevator) { int currentFloor elevator.getCurrentFloor(); if (elevator.getDirection() UP) { // 检查当前方向再无请求 for (int f currentFloor 1; f elevator.getMaxFloor(); f) { if (elevator.hasUpRequestAt(f) || elevator.hasDownRequestAt(f)) { return false; } } return true; // 上方已无请求掉头 } else { for (int f currentFloor - 1; f 0; f--) { if (elevator.hasUpRequestAt(f) || elevator.hasDownRequestAt(f)) { return false; } } return true; } }逻辑说明shouldReverse在每次停靠后判断扫一遍当前方向前方楼层是否存在任何请求不管是上行还是下行。这比 SCAN 的固定到端掉头更灵活。注意判断条件里hasUpRequestAt和hasDownRequestAt都检查——因为下行的电梯停靠 10 层时如果该层同时有下行乘客要去 3 层这个请求顺路同样应该被捎带。4.3 分层调度高层和低层分区减少电梯串区40 层以上的摩天楼单靠 SCAN/LOOK 还不够——高峰期低层乘客频繁上下会把一台电梯拖在低层区间高层的乘客等半天没梯。分层调度把大楼切成多个垂直区域每台电梯限定服务一组楼层。ElevatorSimulation 支持静态分区模式public void setupZones(ListElevator elevators, int totalFloors) { // 双梯 40 层低区 1~20 层高区 21~40 层 elevators.get(0).setServedFloors(IntStream.rangeClosed(1, 20).toArray()); elevators.get(1).setServedFloors(IntStream.rangeClosed(21, 40).toArray()); // 每台电梯的 serveFloors 数组在电梯模型中被检查 // 乘客的 fromFloor 不属于该梯服务区则不响应该跨区请求 }参数说明双梯分区是最常规的配置——低区梯负责 1~20 层高区梯负责 21~40 层。问题在于跨区乘客比如 5 层到 35 层实际建筑中靠换乘大堂解决。仿真里如果简化成跨区请求直接拒绝就会低估高层区的等待时间。比较好的做法是设置 1~2 个换乘层比如 20 层和 21 层之间的转换大堂ElevatorSimulation 里可以用换乘层 两级调度的方式模拟。4.4 暴力枚举下的最优解验证策略上界的试金石ElevatorSimulation 里最有价值的不是 SCAN 或 LOOK 本身而是一个暴力枚举调度器BruteForceScheduler——在乘客数小于某个阈值比如 5 人时枚举所有可能的电梯分配方式找出最优解。这个最优解是其他一切近似策略的上界参考。public class BruteForceScheduler implements ElevatorScheduler { Override public void onPassengerArrive(Passenger p, ListElevator elevators) { if (p.getTotalPending() 5) { // 超过 5 人放弃枚举退化为 SCAN fallbackScheduler.onPassengerArrive(p, elevators); return; } // 枚举所有电梯分配组合计算总等待时间 ListElevator[] assignments enumerateAssignments(p, elevators); Elevator[] best assignments.stream() .min(Comparator.comparingDouble(this::estimateTotalWait)) .orElse(...); assignPassengers(best, p); } }这段代码的价值在于当你要对比 LOOK 和分层调度谁更优时先跑暴力枚举拿到理论下界然后看你的策略离下界还有多少差距。如果 LOOK 的平均等待时间接近枚举下界的 1.2 倍说明策略已经不错如果差了 2 倍以上说明还有明显优化空间。暴力枚举只适用于小规模请求组合电梯调度是 NP-hard 问题请求超 5 人后枚举量会指数爆炸所以仿真里通常只在低峰期启用。5. 避坑指南电梯调度仿真里最常见的六个翻车现场5.1 电梯跑到楼层但乘客不见了现象电梯到达 15 层开门结果没有乘客上轿厢白白停靠一次。 原因乘客和电梯的到达时间戳对不上。乘客到达事件在 100 秒生成但电梯在 95 秒就已经扫描到该楼层并规划停靠还没等乘客走到厅门电梯已经完成上下客关门走了。 解决电梯停靠前必须校验该楼层是否有在等待的乘客而且要加开门等待时间——真实电梯开门后会等待 5~8 秒给乘客反应时间。我在 ElevatorSimulation 里强制加上一个dwellTime参数默认 5 秒只有等待乘客清零或超时才关门。public void onElevatorArrive(Elevator elevator, int floor) { ListPassenger waiting passengerQueue.getWaitingAt(floor, elevator.getDirection()); if (waiting.isEmpty()) { elevator.skipStop(floor); // 没人在等直接跳过 return; } elevator.openDoor(); // dwellTime 内允许持续上客超过 dwellTime 强制关门 scheduleEvent(new DoorCloseEvent(elevator, floor, now dwellTime)); }5.2 电梯来回空跑能耗报表惨不忍睹现象SSTF 策略下电梯刚向上走了两层又来一个低层请求电梯掉头向下如此反复。 原因SSTF 是纯贪心没有考虑电梯当前运动方向与乘客请求方向的一致性。它本质上是磁盘寻道算法的移植磁盘磁头移动成本均匀但电梯有乘客体验问题——来回空跑既耗能又增加噪声。 解决改用方向优先的 LOOK或者在 SSTF 上叠加方向惩罚因子——反向请求的代价加权 1.8 倍再参与比较。这个 1.8 是经验值我试过 1.5 和 2.01.8 在混合客流的仿真中等待时间和能耗的平衡点最好。5.3 泊松到达随机种子不固定对比实验没有说服力现象同一套配置跑 5 次平均等待时间从 72 秒波动到 110 秒策略对比无法得出结论。 原因每次运行new Random()的种子不同乘客到达序列完全不同。SCAN 和 LOOK 在不同随机序列上各有胜负统计噪声盖过了算法差异。 解决所有策略对比实验使用同一组预生成的乘客到达序列。ElevatorSimulation 支持从文件加载乘客到达时间表CSV 格式时间戳,起始楼层,目的楼层跑 5 个策略共用这份 CSV。// 生成一次乘客序列保存到文件 PassengerSequenceGenerator gen new PassengerSequenceGenerator(config); gen.generate().toCsv(passengers_high_peak.csv); // 所有策略加载同一序列 SimulationBuilder builder new SimulationBuilder() .loadPassengers(passengers_high_peak.csv);5.4 换乘层乘客堵在楼梯口不疏散现象分层调度模式下换乘层如 20 层积压大量高区换乘客流等待时间爆炸。 原因低区电梯把乘客送到换乘层后直接返回低区没考虑换乘乘客要再等高区电梯而高区电梯的停靠策略没有优先响应换乘层请求。 解决把换乘层标记为高优先级枢纽楼层。ElevatorSimulation 中我实现了一个HubFloorRequest机制——换乘层产生的新请求在调度器中有更高的权重高区电梯到达换乘层时还应该清空所有下行换乘乘客否则换乘层越积越多。5.5 高峰期轿厢超载但仿真里照单全收现象电梯额定载客 18 人仿真中一台电梯拉了 30 人而且数据还挺好看——因为载得越多效率越高。 原因仿真的电梯模型没有做载重限制乘客可以无限挤入调度效率被人为抬高。 解决在电梯模型中增加实时载重判断轿厢满员时不对沿途请求响应。真实电梯还有个满员直驶模式——满载时跳过所有停靠请求直到有乘客到达目的层释放载重。public boolean canAcceptPassenger(int weight) { if (currentLoad weight ratedLoad) { // 满载不响应任何新请求直达目的层 setExpressMode(true); return false; } return true; }5.6 超长仿真内存溢出事件队列炸了现象仿真时长 8 小时、乘客到达率 λ10 人/分钟时堆内存飙到 2GBGC 频繁。 原因离散事件队列保留了大量已处理事件对象仿真时钟推进后没有及时清理。乘客对象和事件对象在小堆配置下很容易撑满年轻代。 解决每处理完一个事件立即从队列移除并释放引用。另外对历史统计数据做滚动聚合不要每个事件都保留完整历史只保留每分钟聚合后的均值和分位数。6. 用调参日志反推算法边界仿真器里的三个验证习惯6.1 第一次先跑暴力枚举给后续策略画一条基准线这可能是整个 ElevatorSimulation 里最值得做的实验——低峰期λ1 人/分钟5 人以下请求组合切换到暴力枚举调度记录平均等待时间作为理论下界。比如某个 20 层双梯场景下暴力枚举给出的平均等待时间是 18.7 秒而 SCAN 实测 23.4 秒LOOK 实测 21.9 秒——离下界的差距分别是 25% 和 17%。你拿这个数据去写论文或给甲方汇报比干巴巴贴平均值有说服力得多。6.2 调整扇区比例时观察换乘层积压而不只看平均等待双梯 40 层分区时低区 1~20 层和高区 21~40 层看起来均匀但如果你画一下 20 层换乘大厅的等待人数曲线就会发现高峰期换乘层排队人数会持续增长。这时候正确的调参方式是提高高区电梯的调度权重或者把分区改成交错式1~15 16~40再测一轮对比换乘层最大积压人数。平均等待时间会撒谎但换乘层积压不会。6.3 把每分钟的等待时间分位数导出来盯 P95 而不是均值平均等待时间 30 秒不代表用户体验好——如果 5% 的乘客要等 2 分钟以上投诉照样来。我在 ElevatorSimulation 里加了分位数统计输出每次仿真结束生成 CSV包含每分钟的平均等待、P50、P95 和最大等待时间。调参时盯 P95因为 P95 才反映调度策略在突发流量下的表现。有一次 LOOK 的均值比 SCAN 低 5 秒但 P95 高出 20 秒说明 LOOK 在极端情况下对部分乘客极不友好这个信息只看均值永远发现不了。从那以后我每次做策略对比强制自己先导出 P95 和换乘层积压曲线再回头分析均值。这已经成了我跑电梯仿真的固定流程。仿真器的价值不止是跑出几个数字更重要的是逼你直面极端情况下的系统行为——希望这套思路对你也有帮助。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询