一、題目
在校園招聘的季節裡,為了能讓學生們更好地瞭解微軟亞洲研究院各研究組的情況,HR部門計劃為每一個研究組舉辦一次見面會,讓各 個研究組的員工能跟學生相互瞭解和交流。已知有n位學生,他們分別對m個研究組中的若干個感興趣。為了滿足所有學生的要求,HR希望每 個學生都能參加自己感興趣的所有見面會。如果每個見面會的時間為t,那麼,如何安排才能夠使得所有見面會的總時間最短? 最簡單的辦法,就是把m個研究組的見面會時間依次排開,那我們就要用m * t的總時間,我們有10多個研究小組,時間會拖得很長,能否進一步提高效率?
二、分析
此題的官方解法是將問題轉化為一個已知的圖的問題:即圖的最少著色問題。 但有兩點感覺不太好:
- 一是這個問題的轉換感覺角度有些大,不夠平滑。 如果此前沒聽過圖的最少著色問題,那麼是怎麼也不可能想到這兒的。
- 二是關於此題的分析與解法講解非常籠統,尤其是解法,寥寥數語就完了 - 我覺得是沒有把問題講清楚的 - 雖然讀者自己可以閱讀“ 圖的最少著色問題”來獲得更多的瞭解,但既然此題作為單獨的一題存在,我覺得還是講清楚點好。
另外,自己思考了一下,覺的此題不轉化成圖的最少著色問題,通過簡單的遞迴,應該也是可以實現的。
思路分析:(遞迴&合并的思想)
已知有n位學生,m個見面會,且每個學生可以選擇參加任意多個見面會。我們的目的是在沒有衝突的情況下把某幾個見面會的時間重疊起來,同時開。何為衝突,比如學生甲參加了見面會A與B,那麼A與B就是衝突的,因為如果同時開的話,學生甲必然要放棄一個。
那麼,我們可以按以下方式,逐個考慮見面會:
- 對於見面會A,因為其前面沒有見面會,略過;
- 對於見面會B,考慮它和其前面的見面會A是否衝突,如果不衝突,就將B和A合并,繼續考慮C;而另外還有一個分支是不管是否衝突,此時不做合并,直接考慮C;
- 對於見面會C,考慮它和其前面的見面會A,B是否衝突,第一個分支是如果與A不衝突, 就將C和A合并,繼續考慮D;第二個分支是如果與B不衝突, 就將C和B合并,繼續考慮D;而第三格分支還是不做任何合并,直接考慮D
- 按此規則繼續對下一個見面會做考察
- 當對最後一個招聘會做完考察後,記下其時間,程式然後遞迴回溯,會由其他分支繼續考察最後一個招聘會,比較並記錄最短的那個,這樣,當所有的分支都考察過後,記錄的那個最短時間就是全域最短的時間了。
代碼
- 輸入資料可以用一個二維數組input[m][n]來表示,行表示見面會,列表示學生,數組元素表示某學生是否參加該見面會。
- 遞迴過程用當前考慮的見面會控制,當最後一個見面會考慮完之後,就得到一個候選解,程式然後回溯,從另一分支再次進入,考慮最後一個見面會,與之前的候選解比較,儲存較優的那個:候選解包括見面會時間與當前的具體安排
複雜度分析:
可以注意到,我們需要逐個考慮見面會,一共是m個;而在考慮第i個見面會時,我們最多可能會產生出i個分支,不難看出,總問題的規模為m!;
而產生每個分支時需要做的計算是O(n)的衝突檢測與合并,於是,整個的演算法複雜度為:O(m! * n)
#include <iostream>#include <list> #include <vector> using namespace std; // If 2 recruiting meetings are conflicting bool IsConflict(const vector<bool>& v1, const vector<bool>& v2) { for(int i = 0; i < v1.size(); ++i) { if(v1[i] && v2[i]) return true; } return false; } // merge 2 recruiting meetings: v2 will be held at the same time of v1 void Merge(vector<bool>& v1, const vector<bool>& v2) { for(int i = 0; i < v1.size(); ++i) { v1[i] = v1[i] || v2[i]; } } // input: input[m][n], 2d array to represents students' selection of meetings. // row stands for meetings, column stands for students,// and array value stands for if a student select a meeting // check: The meeting to check // curTime: Time required so far // curArrangement: record the information of which meeting is merged with another //meeting// best[output]:The best time // bestArrangement[output] : The best arragement void ArrangeRecruitings(vector<vector<bool> >& input, int check, int& curTime, vector<list<int> >& curArrangement, int& bestTime, vector<list<int> >& bestArrangement){ int m = input.size(); // base cases if(check >= m) { // Save the best one if(curTime < bestTime) { bestTime = curTime; bestArrangement = curArrangement; } return; } else { // recursive cases for(int i = 0; i <= check; ++i) // check: The meeting to check { if(curArrangement[i].empty()) continue; // if already merged with other ones, just skip it if (!IsConflict(input[i], input[check])) { // update the status vector<bool> bkI = input[i]; Merge(input[i], input[check]); curArrangement[check].pop_back(); curArrangement[i].push_back(check); // Consider check one after merge ArrangeRecruitings(input, check+1, curTime, curArrangement, bestTime, bestArrangement); // restore the status;bestarranyment has been saved curArrangement[i].pop_back(); curArrangement[check].push_back(check); input[i] = bkI; } } // Consider check one without any merge ArrangeRecruitings(input, check+1, ++curTime, curArrangement, bestTime, bestArrangement); } } int main() { // 1. Initializing int m = 3; //meetingsint n = 4; //students vector<vector<bool> > input(m, vector<bool>(n, false)); input[0][0] = true; //input[0][1] = true; input[1][1] = true; input[1][2] = true; input[2][2] = true; input[2][3] = true; input[0][3] = true; int curTime = 1; int bestTime = m+1; vector<list<int> > curArrangement(m); vector<list<int> > bestArrangement(m); for(int i = 0; i < m; ++i) curArrangement[i].push_back(i); // 2. Solve ArrangeRecruitings(input, 1, curTime, curArrangement, bestTime, bestArrangement); // 3. Output the result cout << "Totoal Time: " << bestTime << endl; for(int i = 0; i < m; ++i) { cout << i << "): "; if(bestArrangement[i].empty()) { cout << "none" << endl; continue; } for(list<int>::const_iterator it = bestArrangement[i].begin(); it != bestArrangement[i].end(); ++it) cout << *it << "-"; cout << endl; } }
三、第二種解決方案
尋找活動集合S的最大相互相容集合的方法[2],在《演算法導論》貪心演算法一章中有詳細的講解。解決此問題的具體步驟可總結為:
1)把面試集合S中的元素按其結束時間的遞增順序排列;
2)求其最大相互相容面試集合M,並從原創組合S中刪除之;
3)求集合S-M的最大相互相容面試集合Q,並從集合S-M中刪除之。重複此過程直到原始面試集合S中為空白集為止;
4)統計求出的最大相互相容面試集合的個數,即是面試集合S所需的最少面試驗個數。
以上面的面試集合S為例,上述步驟用映像表示如下:每個圖中用矩形框起來的面試為所屬集合的最大相互相容集合,一共有5個,即至少需要5個面試驗。
四、擴充問題一:
研究院一天有N場面試召開(第i場面試:開始時間B[i],結束時間E[i]),每個應聘者只能參加一場面試,為了有一個好的面試環境,N場面試被安排在若干地點。不同的面試在同一時間不能被安排在同一地點。問至少需要多少個面試地點?
分析:
實際上轉化一下,就是求區間的最大重疊次數:即每個時間點的重疊次數,最後找到最大重疊次數即為所求。
書上P60的演算法,比較巧妙,但要注意的是:排序時要用到雙關鍵字比較,當兩個值相等時,屬於時間段開始的一定要排在屬於時間段結束的後面,只有這樣才能保證結果的正確性。(假設[3, 4)和[4, 5)能在同一個地方舉行。書上區間段都是用閉區間,本文採用前閉後開。)
考慮到面試安排的時間一般安排在某個整點、半點鐘或者某刻,可以採用計數的方法,如果都安排在整點,每處理一個區間[a, b),就對[a, b)間的所有整數計數一次。最後從計數結果中找出最大值即可。時間複雜度為O(n)(準確的講,應該是O(k*n),k為區間的最大間隔,k<=24)。如果面試安排時間在某個半點鐘、刻,可以對原來的時間乘以一個整數(比如2或4,這實際上就是桶排序設定桶間隔為0.5或0.25)。
//arr[][0]為面試開始時間,arr[][1]為面試結束時間int max_places (int arr[][2], size_t sz){ if (arr==NULL || sz<1) return 0; const size_t MAX_HOURS=24; int count[MAX_HOURS]={0}; int max=0, j=0; size_t i=0; for (i=0; i<sz; ++i) for (j=arr[i][0]; j<arr[i][1]; ++j) ++count[j];//前閉後開區間 for (i=0; i<MAX_HOURS; ++i) if (count[i]>max) max=count[i]; return max;}