自動排班系統2.0(基於網路流實現的排班系統,附詳細註解)
阿新 • • 發佈:2019-01-27
更新說明:
將輸入優化了下,不必再輸入幹部數和總班數,比較排序部分採用了氣泡排序。
程式碼:
#include <iostream> #include <cstdio> #include <queue> #include <string.h> #include <vector> #include <string> #include <algorithm> #include <sstream> using namespace std; #define arraySize 200 //中心幹部不超過100,每週班數最多50 #define inf 1000000000//無窮大 int capacity[arraySize][arraySize], flow[arraySize][arraySize], max_flow[arraySize], pre[arraySize]; //capacity儲存點之間最大流量,flow儲存點之間當前已經流過的流量 //max_flow儲存每次遍歷過程中的值,pre記錄查詢過程中每個節點的前一節點,用於後續更新 int reflect[100]; //用於標記幹部值哪班,初始值為0,代表還未分配值班時間 int period_info[60][10]; //每班由哪些人來值 int member_amount, period_amount, tmp, start_node, end_node; //幹部數量,班的數量,臨時變數,超級源點,超級匯點 //幹部物件,No-幹部對應序號,name-姓名,period_amount可值班數,store_period儲存可以值的班對應的序號,幹部不超過100人 class member { public: int No; string name; int period_amount; vector <int> store_period; //為冒泡那裡修改的拷貝函式 member() { } member (const member & x) { No=x.No; name=x.name; period_amount=x.period_amount; store_period=x.store_period; } }store_member[100]; //貪心階段幹部的排序,按照空餘時間段少的先安排的策略排序 bool cmp(member a, member b) { return a.period_amount < b.period_amount; } //初始化 void Initialize() { //初始化幹部值班對應關係 memset(reflect, 0, sizeof(reflect)); //初始化班儲存的幹部資訊 memset(period_info, 0, sizeof(period_info)); } //核心演算法,通過設定1個超級源點和1個超級匯點,並把從源點流出和流入源點的值設定為1,從而確保每一班都有一個人值 int Edmonds_Karp(int source, int target)//源點,匯點 { //初始化 queue <int> store; int cur; //ans最大流 此處用於返回看能否保證每一班一個人 int ans=0; //cur當前節點 //flow儲存的是兩個點之間的流量,用於後續判斷幹部和班之間的關係 memset(flow, 0, sizeof(flow)); while (true)//一直尋找增廣路 { memset(max_flow, 0, sizeof(max_flow)); memset(pre, 0, sizeof(pre)); store.push(source); max_flow[source] = inf; while (!store.empty()) { cur = store.front(); store.pop(); for (int next = source; next <= target; next++) { //max_flow[next]恰可以用於標記是否訪問過,同時要保證兩點之間還有剩餘流量 //這個過程中,可能會出現多條可行路徑,但因為匯點只有一個會被先到達的路徑搶佔,故每個過程只能找到一條 if (!max_flow[next] && capacity[cur][next]>flow[cur][next]) { store.push(next); //如果這兩個點之間的值,比之前的最小值還小,則更新 max_flow[next] = min(max_flow[cur], capacity[cur][next] - flow[cur][next]); //記錄前一個節點,用於後續更新 pre[next] = cur; } } } //說明已經找不到增廣路了 if (max_flow[target] == 0)break; //更新操作 for (int u = target; u != source; u = pre[u]) { flow[pre[u]][u] += max_flow[target]; //反向邊 flow[u][pre[u]] -= max_flow[target]; } ans += max_flow[target]; } return ans; } //最後排班資訊顯示 void display() { //i控制每一班的迴圈 for (int i = 1; i <= period_amount; i++) { cout << "The people on duty in Period " << i << "are: "; //j負責輸出該班對應的幾位幹部 for (int j = 0; j < 10; j++) { //如果值非0,就代表還有幹部,輸出 if (period_info[i][j]) //period_info[i][j]儲存的就是幹部的序號,從而取出相應的幹部的姓名 cout << " "<<store_member[period_info[i][j]].name; else break; } cout << endl; } } int main() { //用於儲存在EK演算法之後,還沒有被分配的幹部 vector <member> store_rest; Initialize(); string s;//讀入一行用的變數 int tmpstore;//下面臨時儲存一個幹部可值班數的變數 //模擬系統資料獲取,如若需要每月排班一次可以隨機獲得幹部的輸入順序,即可獲得不同的結果 //建議最後可以新增一個功能,當一個人需要手動調整位置時,可以實現他在哪些班次可以移動 period_amount=15; //班數固定為49班,因為測試需要改成15班,因為自己沒辦法模擬那麼大的測試資料,實際應該為49班 member_amount=0; //幹部數通過計算有幾次輸入獲得,初始值為0 cout << "請按序輸入幹部名字和可值班時段的序號\n"; //member的下標必須從1開始,因為還要設定一個超級源點 while(getline(cin,s))//此處需加一個終止輸入符 ctrl+z { member_amount++; istringstream myIn(s); //輸入幹部姓名 myIn >> store_member[member_amount].name; tmpstore=0;//可以值班數先初始化為0 store_member[member_amount].No = member_amount; //輸入幹部可值班時間段對應序號 while(myIn>>tmp) { tmpstore++; store_member[member_amount].store_period.push_back(tmp); } store_member[member_amount].period_amount=tmpstore; } //設定源點為0,1-member_amount為幹部, //member_amount+1到member_amount+period_amount即為班次 start_node = 0; end_node = member_amount + period_amount + 1; //預設用序號代表班,從1到period_amount //建圖 //初始化,預設兩點間有通路為1,無通路為0。 memset(capacity, 0, sizeof(capacity)); //超級源點為0,超級匯點為member_amount+period_amount+1 //超級源點到每個幹部間連一條權值為1的邊,代表每個幹部值1班 for (int i = 1; i <= member_amount; i++) { capacity[0][i] = 1; } //每班到超級匯點之間連一條權值為1的邊,從而先確保每一班保證有一個幹部值 for (int i = member_amount+1; i <= end_node-1; i++) { capacity[i][end_node] = 1; } for (int i = 1; i <= member_amount; i++) { for (int j = 0; j < store_member[i].period_amount; j++) { //幹部和班之間權值置1,代表幹部到班之間有一條路 //store_member[i].store_period[j]儲存的是第i個幹部的第j班可以值哪個班加上member_amount的值就可以表示圖中的點 capacity[i][store_member[i].store_period[j] + member_amount] = 1; } } //呼叫EK演算法,先確保每一班有一人值。 int check=Edmonds_Karp(start_node, end_node); //check用來檢驗是否能夠保證每班至少1人,若不等於班數則代表不可以確保,不過這種情況幾乎不可能發生 /*此處或許可以更加嚴密,如出現這種情況,就讓某些幹部值兩班,出現這種情況,只要將源點到每個幹部的權值設為2 保留每個班次到匯點的權值為1,細節部分需要稍作修改*/ if (check != period_amount) { cout << "無法保障每班1人\n"; return 0; } //給已經分配好的值班確認關係 for (int i = member_amount + 1; i <= end_node - 1;i++) { for (int j = 1; j <= member_amount; j++) { //flow的值代表第j個幹部是否值第(i-member_amount)個班,如果為1,則建立關係 if (flow[j][i] == 1) { reflect[j] = (i - member_amount); break; } } } //處理已經分配好的幹部和對應班的關係 for (int i = 1; i <= member_amount; i++) { if(reflect[i])//如果第i個幹部已經被分配了1班 tmp = reflect[i]; else continue; //否則跳過 for (int j = 0; j < 10; j++) { //找到一個空值,把這個幹部放進去 if (period_info[tmp][j] == 0) { period_info[tmp][j] = i; break; } } } //貪心階段 for (int i = 1; i <= member_amount; i++) { //還未被安排 if (reflect[i] == 0) { store_rest.push_back(store_member[i]); } } //按照自定義的貪心策略排序,已修改為冒泡 member temp; for(int i=1;i<store_rest.size();i++) { for(int j=store_rest.size()-1;j>=i;j--) { if(store_rest[j].period_amount<store_rest[j-1].period_amount) { temp=store_rest[j]; store_rest[j]=store_rest[j-1]; store_rest[j-1]=temp; } } } int p, minn,cnt; //p用來記錄該幹部的哪個可值班中現有人數最少的那個班的序號 //minn用來記錄,該幹部可值班中最小的現有人數 for (int i = 0; i < store_rest.size(); i++)//迴圈剩下的幹部 { p = 0; minn = inf; //迴圈該幹部的可值班,找出他可值班中哪個班現有人數最少,並將他安排到那個班次 //同時注意及時更新班次資訊,否則會在貪心選擇時出錯 for (int j = 0; j < store_rest[i].period_amount; j++) { cnt = 0; for (int k = 0; k < 10; k++) { //store_rest[i].store_period[j][k]剩下的第i個幹部,他可值的第j個班的班的序號 if (period_info[store_rest[i].store_period[j]][k]) { cnt++; } else break; } if (cnt < minn) { minn = cnt; //更新最小班的位置 p = store_rest[i].store_period[j]; } } //更新 reflect[store_rest[i].No] = p; for (int k = 0; k < 10; k++) { if (period_info[p][k])continue; else { //更新班次資訊 period_info[p][k]=store_rest[i].No; break; } } } //顯示 display(); system("pause"); return 0; }