在像芝加哥,紐約,東京,新加坡,香港等大城市裡,每天都會有上百萬的人通過電梯離開他們的大樓。但是我們卻很少考慮電梯是如何排程來提供服務的,尤其是在人流高峰期,這個時候辦公樓裡的大多數人都會企圖在大約一個小時左右離開。
關於這方面主題(基於乘客等待時間的電梯分配系統)和研究(電梯流量模擬)的演算法至少有一個專利,並且出現在Quaro上。曾經在一次面試中,面試官問我我會如何排程電梯。這點我之前有提到過:
“ 假如有十層樓,每層都有相同數量的人,一共有三部電梯並且沒有樓梯。你將如何分配電梯來實現效能最優,及最小化每一層的等待時間?”
我很喜歡這個面試問題。我覺得這個問題很有挑戰性而且你可以想得儘量深入,但是也應該足夠直接以便於你下手,產生某些解決方案。沒有絞盡腦汁想一個月來模擬真實場景,在這篇文章中,我會嘗試解決一個簡化的電梯排程問題,類似於上面的面試問題。
問題描述
創造一個可用於現實生活中電梯執行的演算法(很顯然,這類演算法已被申請專利)是有難度的。因此,我會努力解決一些與我面試問題類似的問題,我會做一點輕微變換:
設計一個使大樓裡所有人等待時間最短的演算法,同時要考慮每一層的負載量。假定每一層人數相同且每層的人以同樣的方式使用電梯。假設每天有幾個小時是“高峰時段”,演算法需要提供一種最“公平”的方式來將電梯分配到不同的樓層。
上面是對問題的整體描述,但是如果我們將問題分解,該問題包含以下條件:
- 樓層數量任意
- 電梯數量任意
- 給定高峰時段
- 我們必須通過某種關於負載和時間的函式來分配電梯
一些我們需要考慮但是未說明的變數或者常量:
- 每層人數:100人
- 電梯通過一層的時間(不停):5秒鐘
- 每層的等待時間:20秒鐘
我給上面的變數賦值,而且儘管“電梯通過每一層的時間”有可能不是線性的(即電梯需要花時間從停止位置開始加速),但是我們還是這樣假設。雖然做這些假設可能會“過度簡化”問題,但是我相信這篇文章已經可以滿足面試要求而且可以作為一個很好的契入點,來引發更深的思考和討論。
注意,我並沒有考慮電梯的容量,在這方面我要做個很大膽的假設。我的假設(貫穿整個方案)是每個電梯的容量無限大。很顯然這是不正確的,但是一旦我們有了解決方案,我認為增加像這樣的宣告會容易得多:
如果電梯滿了,回到較下的樓層;釋放乘客後再返回原來的樓層。
我可能會另外寫一篇文章放到這個部落格裡,或者我會通過我的郵件列表來發布。不管哪種情況,我希望有人能想辦法自己來解決!
電梯分配演算法
這可能不是最佳方案,儘管它有可能效果不錯。你如果找到一個更好的方案,請分享!
正如上面圖片顯示的,我會給特定的樓層指定 一個具體的電梯,我稱之為區域電梯分配。這個想法在於我們可以獲得每層的平均等待時間和每層的平均負載量。
我的這個特殊方法是基於我對每個電梯形成一個迴路(即在電梯迴圈裡經過所有樓層,例如:0->1->2->0)所花費時間的一些觀察。我們所有要知道的就是下面這些,來計算一個電梯完成一次迴路所需的時間:
- 經過一層樓的時間乘以往返中最高樓層數乘以2(上和下),在我們問題中:(5 seconds * <maxFloor> * 2)
- 從最低樓層到最高樓層之間電梯停的層數乘以每次停等所花費的時間,在我們問題中:(20 seconds * <floorsServiced>)
總的往返時間:
elevatorsCircuitTime = (5*<maxFloor>*2)+(20*<floorsServiced>)
使用下面方程計算一個往返中電梯的平均載人數:
avgElevatorLoad = <elevatorsCircuitTime>*<floorsServiced>*<peoplePerFloor>/<rushHour>
變數 rushHour等於完成運輸一個高峰時間段所花費時間,floorsServiced等於電梯所停的樓層數,peoplePerFloor指給定樓層的人數。因為我們已經計算出了電梯往返時間,所以我們可以利用這個時間和平均負載效能來實現我們的演算法。
我給這個問題的解決方案需要兩個陣列:
大樓的表示:大樓陣列中每個元素代表每層的人數。陣列的每個單元表示一層樓。例如[100 100 100]可以表示一棟四層樓,只有高三層的人需要使用電梯。
電梯的表示:電梯陣列中的每個單元代表該電梯在它迴路中所能到達的最高層(為了簡化我把0放進第0個單元)。例如,[0,2,3]表示兩個電梯,1號電梯(在1號單元)運載乘客從2樓到1樓到0,2號電梯(在2號單元)運載乘客從3樓到0。
初始時,1號陣列(表示大樓)為空,然後每次我給該陣列“增加一層”時,我給這一層分配一個電梯。如你所見,這種分配是可以改變的,但是它會遵從一個相似的形式。擁有最小往返的電梯迴路會被分配該新的樓層,除非需要涉及效能問題。我增加了一個小方程:
elevatorCircuitTime + ((elevatorCircuitTime / 100) * elevatorsAvgLoad)
因為elevatorCircuitTime 是一個整數,除非往返時間超過100秒(這對電梯來說是一個很長的時間),將elevatorsAvgLoad乘到這個方程中。我們的問題描述非常模糊,而且考慮到上面的方程,我的解決方案同樣地的確有一些模糊之處。同樣地,我用來分配樓層給電梯的函式十分任意,但是在負載管理中卻十分有效(儘管可能會有更好的方案)。
下面是層架樓層函式的實現程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# (Index * 5 seconds) + (20 seconds * (Index - PrevIndex)) # If previous elevators loops/stops add up to be greater than, # (timePerFloor * 2) + timePerWait, then increase floor of previous # elevators loop. i.e. elevator[2]+=1 # e represents elevatorArray def addFloor(e): best = 99999 for i in range(1, len(e)): cirTime, avgCarry = eleLoop(e, i) if cirTime + ((cirTime / 100) * avgCarry) < best: elevatorNumber = i best = cirTime + ((cirTime / 100) * avgCarry) for i in range(elevatorNumber, len(e)): e[i] += 1 return e |
注意,每次一個 “elevatorNumber”被選擇後,所有在 “elevatorNumber”之上的電梯所到達的最高樓層數加1:
for i in range(elevatorNumber, len(e)):
e[i] += 1
這是因為在被選擇之上的每個電梯往返所到達的最高的樓層會加1,但是我們只希望增加一個額外的電梯到被選擇的電梯迴路中。附加函式eleLoop(e, i)很容易確定在被選擇迴路中的往返時間和平均載客量。
一旦我們有了增加樓層的函式,我就可以建構函式來迴圈通過和建立樓層。注意,在本情況中,所有樓層被設定為統一的。這樣如果本問題被擴充套件為每層人數不同的情況,也會相對容易考慮。
1 2 3 4 5 6 7 8 9 10 |
# Allocate elevators # Elevator[] represents the starting # group of stops. def elevatorAllocation(building, elevatorCount): elevator = [] for i in range(elevatorCount + 1): elevator.append(0) for i in range(1, floorCount): elevator = addFloor(elevator) printeleLoop(elevator) |
以上便是演算法分配部分的大體。本演算法相對直接而且留有一定量的的改進空間,我將這部分留給大家來解決!
實現|Python語言
如果將演算法的各部分拼接起來,再加一些額外的函式來列印資料,建立一個小巧的模擬器,我們就可以獲得一個很酷的小程式(大家可以從我的github中fork下來或者檢視)。
變數:
- 10層樓
- 3部電梯
- 1個高峰時段
- 通過一層耗時5秒
- 電梯需要停等時,每次停20秒
- 每層100人
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113# Sets up the building, filling all the floors with peopledef fillBuilding():building = []for i in range(floorCount - 1):building.append(peoplePerFloor)return building# Determines the time for circuit (cirTime),# as well as average carrying capacity per circuit.# Given e - array of elevators, which holds the highest# serviced floor, and i the current index of e.def eleLoop(e, i):floorsServiced = e[i] - e[i-1] + 1cirTime = timePerFloor * e[i] * 2cirTime += timePerWait * floorsServicedavgCarry = cirTime * peoplePerFloor / rushHour * floorsServicedreturn cirTime, avgCarry# (Index * 5 seconds) + (20 seconds * (Index - PrevIndex))# If previous elevators loops/stops add up to be greater than,# (timePerFloor * 2) + timePerWait, then increase floor of previous# elevators loop. i.e. elevator[2]+=1def addFloor(e):best = 9999for i in range(1, len(e)):cirTime, avgCarry = eleLoop(e, i)if cirTime + ((cirTime / 100) * avgCarry) < best:elevatorNumber = ibest = cirTime + ((cirTime / 100) * avgCarry)for i in range(elevatorNumber, len(e)):e[i] += 1return e# Prints the population of the buildings floor as an array.def printApprox(building):str = '[ 'for i in range(len(building)):str += '%06.3f ' % building[i]str += ']'print str# Prints the circuit(s) for each of the elevatorsdef printeleLoop(e):print ''print eprint ''for i in range(1, len(e)):floorsServiced = e[i] - e[i-1] + 1curr = timePerFloor * e[i] * 2curr += timePerWait * floorsServicedavgCarry = curr * peoplePerFloor / rushHour * floorsServicedstr = 'Elevator #%d, time for loop %d seconds, ' % (i, curr)str += 'carrying an average of 'str += '%3.2f people per carry' % avgCarryprint strprint ''# Allocate elevators# Elevator[] represents the starting# group of stops.def elevatorAllocation(building, elevatorCount):elevator = []for i in range(elevatorCount + 1):elevator.append(0)for i in range(1, floorCount):elevator = addFloor(elevator)printeleLoop(elevator)return elevator# Simulates the building being emptied at rush hourdef simulate(e, building):str = '[ 'for floor in range(len(building)):str += 'floor%2d ' % (floor + 1)str += ']'print streCircuit = []for i in range(len(e)):curr, avgCarry = eleLoop(e, i)eCircuit.append(float(curr))emptyFloors = 0iteration = 0finalFloor = 0while emptyFloors < len(building):emptyFloors = 0iteration += 1for i in range(1, len(e)):for j in range(e[i-1], e[i]):if building[j] > 0.0:persons = eCircuit[i] * peoplePerFloor / rushHourbuilding[j] = building[j] - personsif 0 >= building[j]:building[j] = 0.0emptyFloors += 1finalFloor = jprintApprox(building)print ''# Find the final elevator on circuit, prints timefor i in range(len(e)):if e[i] > finalFloor:iteration = eCircuit[i] * iteration / 60print 'Total Time: %d minutes\n' % (iteration)# ___ MAIN ____building = fillBuilding()elevator = elevatorAllocation(building, elevatorCount)simulate(elevator, building)
輸出:
[0, 4, 7, 9]
- #1電梯:往返用時140秒,平均每次載客19.44人
- #2電梯:往返用時150秒,平均每次載客16.67人
- #3電梯:往返用時150秒,平均每次載客12.50人
總用時:65分鐘
電梯往返時,每層的平均人數變化情況:
如你所見,我的演算法提供了一個良好的但不是最佳的方案(在本情況下)。本演算法雖然還有很大的改進空間(我將這部分留給你們來挑戰),但是它是一個好的開始。
計算執行時
計算本演算法的時間和空間要求會有一點難度,但也不是很難。演算法的執行時取決於以下三個因素:
- k:最大回路,人數
- n:在最大回路中需要服務樓層的起始人數
- m:總的樓層數
(1)執行時:O(m * (n/k))
‘n / k’ :決定了電梯需要的最大往返次數;‘m’ :這一項是因為在電梯往返過程中需要對每一層進行迭代。在本情況中,我們忽略了初始化大樓陣列這一步驟,該陣列代表每一層的人數,因為這一項不是執行時(m*(n/k) + m)的主要項。
最大空間需求很直接:
- e:電梯數量
- f:樓層數量
(2)記憶體要求:O(e + f)
將以上因素整合起來:
- k:最大回路,人數
- n:在最大回路中需要服務樓層的起始人數
- m:總的樓層數
- e:電梯數量
- f:樓層數量
- 執行時間:O(m * (n / k))
- 記憶體要求:O(e + f)
結束語
我認識到這並不是一個最佳方案,然而它確實解決了問題。我挑戰你們來進行評論,從我的github中fork,改進我的程式碼或者是在自己的文章中寫下自己的方案。我認為這是一個很有趣的問題,而且同你們電腦內部的資源分配類似,我還寫了篇基本的排程處理導論的文章,如果你們感興趣,可以來讀一讀。我同樣十分樂意看到一個不同的(希望更好的)方案,所以如果你們想到了更好的方案,一定不要忘記寫下來。
我本來確實打算另寫一篇文章來更深地挖掘這個問題,並提供一個更接近現實世界應用的演算法,但是我還沒有確定日期(可能是幾天,幾周或者是幾個月)。我希望你們喜歡這篇文章而且我也很樂意聽聽你們的想法,所以不要猶豫是否評論和跟我發郵件,謝謝!