本內容來源於《趣學算法》,在線章節:http://www.epubit.com.cn/book/details/4825
高級鐘點祕書——會議安排
“鐘點祕書”爲客戶提供有償服務的方式一般是:採用電話、電傳、上網等“遙控”式服務,或親自到客戶公司處理部分業務。其服務對象主要有三類:一是外地前來考察商務經營、項目投資的商人或政要人員,他們由於初來乍到,急需有經驗和熟悉本地情況的祕書幫忙;二是前來開展短暫商務活動,或召開小型資訊發佈會的國外客商;三是本地一些請不起長期祕書的企、事業單位。這些客戶普遍認爲:請“鐘點祕書”,一則可免去專門租樓請人的大筆開銷;二則可根據開展的商務活動請有某方面專長的可用人才;三則由於對方是臨時僱用關係,工作效率往往比固定的祕書更高。據調查,在上海“鐘點祕書”的行情日趨看好。對此,業內人士認爲:爲了便於管理,各大城市有必要組建若干家“鐘點祕書服務公司”,通過會員制的形式,爲衆多客戶提供規範、優良、全面的服務,這也是建設國際化大都市所必需的。
某跨國公司總裁正分身無術,爲一大堆會議時間表焦頭爛額,希望高級鐘點祕書能做出合理的安排,能在有限的時間內召開更多的會議。
圖2-6 高級鐘點祕書
2.4.1 問題分析
這是一個典型的會議安排問題,會議安排的目的是能在有限的時間內召開更多的會議(任何兩個會議不能同時進行)。在會議安排中,每個會議i都有起始時間bi和結束時間ei,且bi<ei,即一個會議進行的時間爲半開區間[bi,ei)。如果[bi,ei)與[bj,ej)均在“有限的時間內”,且不相交,則稱會議i與會議j相容的。也就是說,當biej或bjei時,會議i與會議j相容。會議安排問題要求在所給的會議集合中選出最大的相容活動子集,即儘可能在有限的時間內召開更多的會議。
在這個問題中,“有限的時間內(這段時間應該是連續的)”是其中的一個限制條件,也應該是有一個起始時間和一個結束時間(簡單化,起始時間可以是會議最早開始的時間,結束時間可以是會議最晚結束的時間),任務就是實現召開更多的滿足在這個“有限的時間內”等待安排的會議,會議時間表如表2-6所示。
表2-6 會議時間表
會議i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
開始時間bi | 8 | 9 | 10 | 11 | 13 | 14 | 15 | 17 | 18 | 16 |
結束時間ei | 10 | 11 | 15 | 14 | 16 | 17 | 17 | 18 | 20 | 19 |
會議安排的時間段如圖2-7所示。
圖2-7 會議安排時間段
從圖2-7中可以看出,{會議1,會議4,會議6,會議8,會議9},{會議2,會議4,會議7,會議8,會議9}都是能安排最多的會議集合。
要讓會議數最多,我們需要選擇最多的不相交時間段。我們可以嘗試貪心策略:
(1)每次從剩下未安排的會議中選擇會議具有最早開始時間且與已安排的會議相容的會議安排,以增大時間資源的利用率。
(2)每次從剩下未安排的會議中選擇持續時間最短且與已安排的會議相容的會議安排,這樣可以安排更多一些的會議。
(3)每次從剩下未安排的會議中選擇具有最早結束時間且與已安排的會議相容的會議安排,這樣可以儘快安排下一個會議。
思考一下,如果選擇最早開始時間,則如果會議持續時間很長,例如8點開始,卻要持續12個小時,這樣一天就只能安排一個會議;如果選擇持續時間最短,則可能開始時間很晚,例如19點開始,20點結束,這樣也只能安排一個會議,所以我們最好選擇那些開始時間要早,而且持續時間短的會議,即最早開始時間+持續時間最短,就是最早結束時間。
因此採用第(3)種貪心策略,每次從剩下的會議中選擇具有最早結束時間且與已安排的會議相容的會議安排。
2.4.2 算法設計
(1)初始化:將n個會議的開始時間、結束時間存放在結構體數組中(想一想,爲什麼不用兩個一維數組分別存儲?),如果需要知道選中了哪些會議,還需要在結構體中增加會議編號,然後按結束時間從小到大排序(非遞減),結束時間相等時,按開始時間從大到小排序(非遞增);
(2)根據貪心策略就是選擇第一個具有最早結束時間的會議,用last記錄剛選中會議的結束時間;
(3)選擇第一個會議之後,依次從剩下未安排的會議中選擇,如果會議i開始時間大於等於最後一個選中的會議的結束時間last,那麼會議i與已選中的會議相容,可以安排,更新last爲剛選中會議的結束時間;否則,捨棄會議i,檢查下一個會議是否可以安排。
2.4.3 完美圖解
1.原始的會議時間表(見表2-7):
表2-7 原始會議時間表
會議num | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
開始時間beg | 3 | 1 | 5 | 2 | 5 | 3 | 8 | 6 | 8 | 12 |
結束時間end | 6 | 4 | 7 | 5 | 9 | 8 | 11 | 10 | 12 | 14 |
2.排序後的會議時間表(見表2-8):
表2-8 排序後的會議時間表
會議num | 2 | 4 | 1 | 3 | 6 | 5 | 8 | 7 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
開始時間beg | 1 | 2 | 3 | 5 | 3 | 5 | 6 | 8 | 8 | 12 |
結束時間end | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 |
3.貪心選擇過程
(1)首先選擇排序後的第一個會議即最早結束的會議(編號爲2),用last記錄最後一個被選中會議的結束時間,last=4。
(2)檢查餘下的會議,找到第一個開始時間大於等於 last(last=4)的會議,子問題轉化爲從該會議開始,餘下的所有會議。如表2-9所示。
表2-9 會議時間表
從子問題中,選擇第一個會議即最早結束的會議(編號爲3),更新last爲剛選中會議的結束時間last=7。
(3)檢查餘下的會議,找到第一個開始時間大於等於last(last=7)的會議,子問題轉化爲從該會議開始,餘下的所有會議。如表2-10所示。
表2-10 會議時間表
從子問題中,選擇第一個會議即最早結束的會議(編號爲 7),更新 last 爲剛選中會議的結束時間last=11。
(4)檢查餘下的會議,找到第一個開始時間大於等於last(last=11)的會議,子問題轉化爲從該會議開始,餘下的所有會議。如表2-11所示。
表2-11 會議時間表
從子問題中,選擇第一個會議即最早結束的會議(編號爲10),更新last爲剛選中會議的結束時間last=14;所有會議檢查完畢,算法結束。如表2-12所示。
4.構造最優解
從貪心選擇的結果,可以看出,被選中的會議編號爲{2,3,7,10},可以安排的會議數量最多爲4,如表2-12所示。
表2-12 會議時間表
2.4.4 僞代碼詳解
(1)數據結構定義
以下C++程序代碼中,結構體meet中定義了beg表示會議的開始時間,end表示會議的結束時間,會議meet的數據結構:
struct Meet
{
int beg; //會議的開始時間
int end; //會議的結束時間
} meet[1000];
(2)對會議按照結束時間非遞減排序
我們採用C++中自帶的sort函數,自定義比較函數的辦法,實現會議排序,按結束時間從小到大排序(非遞減),結束時間相等時,按開始時間從大到小排序(非遞增):
bool cmp(Meet x,Meet y)
{
if(x.end==y.end) //結束時間相等時
return x.beg>y.beg; //按開始時間從大到小排序
return x.end<y.end; //按結束時間從小到大排序
}
sort(meet,meet+n,cmp);
(3)會議安排問題的貪心算法求解
在會議按結束時間非遞減排序的基礎上,首先選中第一個會議,用last變量記錄剛剛被選中會議的結束時間。下一個會議的開始時間與last比較,如果大於等於last,則選中。每次選中一個會議,更新last爲最後一個被選中會議的結束時間,被選中的會議數ans加1;如果會議的開始時間不大於等於last,繼續考查下一個會議,直到所有會議考查完畢。
int ans=1; //用來記錄可以安排會議的個數,初始時選中了第一個會議
int last = meet[0].end; //last記錄第一個會議的結束時間
for( i = 1;i < n; i++) //依次檢查每個會議
{
if(meet[i].beg > =last)
{ //如果會議i開始時間大於等於最後一個選中的會議的結束時間
ans++;
last = meet[i].end; //更新last爲最後一個選中會議的結束時間
}
}
return ans; //返回可以安排的會議最大數
上面介紹的程序中,只是返回了可以安排的會議最大數,而不知道安排了哪些會議,這顯然是不滿足需要的。我們可以改進一下,在會議結構體meet中添加會議編號num變量,選中會議時,顯示選中了第幾個會議。
2.4.5 實戰演練
//program 2-3
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
struct Meet
{
int beg; //會議的開始時間
int end; //會議的結束時間
int num; //記錄會議的編號
}meet[1000]; //會議的最大個數爲1000
class setMeet{
public:
void init();
void solve();
private:
int n,ans; // n:會議總數 ans: 最大的安排會議總數
};
//讀入數據
void setMeet::init()
{
int s,e;
cout <<"輸入會議總數:"<<endl;
cin >> n;
int i;
cout <<"輸入會議的開始時間和結束時間,以空格分開:"<<endl;
for(i=0;i<n;++i)
{
cin>>s>>e;
meet[i].beg=s;
meet[i].end=e;
meet[i].num=i+1;
}
}
bool cmp(Meet x,Meet y)
{
if (x.end == y.end)
return x.beg > y.beg;
return x.end < y.end;
}
void setMeet::solve()
{
sort(meet,meet+n,cmp); //對會議按結束時間排序
cout <<"排完序的會議時間如下:"<<endl;
int i;
cout <<"會議編號"<<" 開始時間 "<<" 結束時間"<<endl;
for(i=0; i<n;i++)
{
cout<< " " << meet[i].num<<"\t\t"<<meet[i].beg <<"\t"<< meet[i].end << endl;
}
cout <<"-------------------------------------------------"<<endl;
cout << "選擇的會議的過程:" <<endl;
cout <<" 選擇第"<< meet[0].num<<"個會議" << endl;//選中了第一個會議
ans=1;
int last = meet[0].end; //記錄剛剛被選中會議的結束時間
for( i = 1;i < n;++i)
{
if(meet[i].beg>=last)
{ //如果會議i開始時間大於等於最後一個選中的會議的結束時間
ans++;
last = meet[i].end;
cout <<" 選擇第"<<meet[i].num<<"個會議"<<endl;
}
}
cout <<"最多可以安排" <<ans << "個會議"<<endl;
}
int main()
{
setMeet sm;
sm.init();//讀入數據
sm.solve();//貪心算法求解
return 0;
}
算法實現和測試
(1)運行環境
Code::Blocks
(2)輸入
輸入會議總數:
10
輸入會議的開始時間和結束時間,以空格分開:
3 6
1 4
5 7
2 5
5 9
3 8
8 11
6 10
8 12
12 14
(3)輸出
排完序的會議時間如下:
會議編號 開始時間 結束時間
2 1 4
4 2 5
1 3 6
3 5 7
6 3 8
5 5 9
8 6 10
7 8 11
9 8 12
10 12 14
-------------------------------------------------
選擇的會議的過程:
選擇第2個會議
選擇第3個會議
選擇第7個會議
選擇第10個會議
最多可以安排4個會議
使用上面貪心算法可得,選擇的會議是第2、3、7、10個會議,輸出最優值是4。
2.4.6 算法解析及優化拓展
1.算法複雜度分析
(1)時間複雜度:在該算法中,問題的規模就是會議總個數n。顯然,執行次數隨問題規模的增大而變化。首先在成員函數setMeet::init()中,輸入n個結構體數據。輸入作爲基本語句,顯然,共執行n次。而後在調用成員函數setMeet::solve()中進行排序,易知sort排序函數的平均時間複雜度爲O(nlogn)。隨後進行選擇會議,貢獻最大的爲if(meet[i].beg>=last)語句,時間複雜度爲O(n),總時間複雜度爲O(n +nlogn)= O(nlogn)。
(2)空間複雜度:在該算法中,meet[]結構體數組爲輸入數據,不計算在空間複雜度內。輔助空間有i、n、ans等變量,則該程序空間複雜度爲常數階,即O(1)。
2.算法優化拓展
想一想,你有沒有更好的辦法來處理此問題,比如有更小的算法時間複雜度?