趣味演算法題——電梯排程問題
這是《程式設計之美》中的一道題,剛開始題目比較簡單,但是逐步推進之後的問題也有些難度,這樣由簡單到難的一步步深入的思想比較值得學習。
最初的問題
假如電梯在高峰期間只允許在某一層停留,所有的乘客在一樓上電梯,到達某層後,所有乘客從電梯下來,到達自己要去的樓層,在一樓的時候所有乘客選出自己要去的樓層,電梯根據所有乘客選擇的樓層資訊得出要停留的樓層。
那麼,電梯停留在那一層,能夠保證這次乘坐電梯的所有乘客爬樓梯的層數之和最少。
問題的分析和解法
該問題本質上是一個優化問題,首先為則個問題找到一個合適的抽象模型。從問題中可以看出,有兩個因素會影響到最後的結構:乘客的數目即需要停留的樓層,因此,我們可以從統計到達各層的乘客數目開始分析。
假設樓層總共有N層,電梯停留在X層,要去第i層的乘客總數目是Tot[i],這樣,所爬樓梯的總數就可以表示出老。
因此,我們就是要找到一個整數X使得這個總數的值最小。
解法
首先思考簡單解法,可以從第1層開始列舉X一直到第N層,然後再計算出如果電梯在第X層的話,所有乘客總共需要爬多少層樓。這是最為直接的一個解法。可以看出,這個演算法需要兩重迴圈來完成計算
int n=0;//這個表示電梯所在的最高樓層
int nPerson[] = new int[n+1];//這個陣列表示要去每一層的乘客數
//上面的兩個變數應該是給出的,這裡只是示意程式碼沒有初始化。
int nFloor,nMinFloor,nTargetFloor;//分別表示電梯停在第n層時候的結果,目前為止的最小結果,最小結果所停的樓層。
nTargetFloor = -1 ;//考慮到初始化最小結果和最小結果所砸的樓層,所以先設定為一個非法值
//迴圈遍歷每個樓層的結果,算出本層的結果,如果比之前結果小,就替換之前結果
for(int i = 1; i <= n; i++){
nFloor = 0;
//計算本層以下的樓層的人的總消耗
for(int j =1; j < i; j++){
nFloor += nPerson[j]*(i-j);
}
//計算本層以上的樓層的人的總消耗
for (int j = i+1; j <= n; j++){
nFloor += nPerson[j]*(j - i);
}
//如果比之前結果小,或者還沒初始化,就用本層結果替換之前結果。
if(nTargetFloor == -1 || nTargetFloor > nFloor){
nMinFloor = nFloor;
nTargetFloor = i ;
}
}
這個基本解法的時間複雜度為O(N²)
解法的優化
我們希望儘可能地減少演算法的時間複雜度。那麼,是否有可能在低於O(N²)
的規則下求出這個問題的解呢?
我們可以有如下的思路:
假設電梯停在第i層,顯然我們可以計算出所有乘客總共要爬樓梯的層數Y。如果有N1個乘客目的樓層在第i層樓一下,有N2個乘客在第i層樓,還有N3個乘客在第i層以上。這個時候,如果電梯改停在第i-1層,所有目的地在第i-1層一下的蹭課可以少爬一層,總共可以少爬N1層,所有目的地在i層及以上的乘客都需要多爬1層,總共需要多爬N2+N3層。因此,乘客總共需要爬的層數為Y-N1+(N2+N3)=Y-(N1-N2-N3)層。
反之,如果電梯在i+1層停那麼乘客總共需要爬的層數為Y+(N1+N2-N3)層。由此可見,當N1>N2+N3時,電梯在第i-1層停更好,乘客走的樓層數減少N1-N2-N3層;當N1+N2
int n=0;//這個表示電梯所在的最高樓層
int nPerson[] = new int[n+1];//這個陣列表示要去每一層的乘客數
//上面的兩個變數應該是給出的,這裡只是示意程式碼沒有初始化。
int nMinFloor = 0,nTargetFloor = 1;//分別表示目前為止的最小結果,最小結果所停的樓層。
int N1 = 0 , N2 = nPerson[1] , N3 = 0;//分析中所說的N1,N2,N3建立和初始化
//求出第二層時候的N3值並初始化
for(int k = 2; k <= n;k++){
N3 += nPerson[k];
nMinFloor += nPerson[k] *(k-1);
}
//從第二層開始按照上述方法求出最優值
for(int i = 2; i <= n ;i++){
if( (N1 + N2) < N3){//如果i+1層更好,就替換最小結果,
nTargetFloor = i ;
nMinFloor += (N1 +N2 - N3);
N1 += N2;
N2 = nPerson[i];
N3 -= nPerson[i];
}else{//如果上面的樓層不會更好,就不用再查看了。
break;
}
}
問題擴充套件
往上爬樓梯,總比往下走是要累的,假設往上爬一個樓層,需要消耗k個單位的能力,而往下走需要消耗一個單位的能量,那麼如果題目條件改為讓所有人消耗的能量最少,這個問題怎麼解決呢?
分析
其實這個問題並沒有變難太多,只是在前面的思考的前提下放入一個新的考慮:上下樓消耗能量。這個需要在計算的時候上樓就要乘以一個引數K(因為能量是下樓的k倍嘛),其他的考慮都還一樣。
int n=0,k = 1;//這個表示電梯所在的最高樓層和上樓梯需要消耗的能量
int nPerson[] = new int[n+1];//這個陣列表示要去每一層的乘客數
//上面的變數應該是給出的,這裡只是示意程式碼沒有初始化。
int nMinFloor = 0,nTargetFloor = 1;//分別表示目前為止的最小結果,最小結果所停的樓層。
int N1 = 0 , N2 = nPerson[1] , N3 = 0;//分析中所說的N1,N2,N3建立和初始化
//求出第二層時候的N3值並初始化
for(int m = 2; m <= n;m++){
N3 += nPerson[m];
nMinFloor += nPerson[m] *(m-1);
}
//從第二層開始按照上述方法求出最優值
for(int i = 2; i <= n ;i++){
if( k*(N1 + N2) < N3){//如果i+1層更好,就替換最小結果,
nTargetFloor = i ;
nMinFloor += (k*(N1 +N2) - N3);
}
N1 += N2;
N2 = nPerson[i];
N3 -= nPerson[i];
}
補充說明
其實目前為止的邏輯過於不切實際,因為實際中在高層電梯中不會出現只停一層的情況,而且如果有類似的政策的話,比較低的樓層的人會直接選擇爬樓去而不去按電梯(即使是統計好人數也會在計算總體的消耗上有很大的不同)。