1. 程式人生 > >自動排班系統2.0(基於網路流實現的排班系統,附詳細註解)

自動排班系統2.0(基於網路流實現的排班系統,附詳細註解)

更新說明:

    將輸入優化了下,不必再輸入幹部數和總班數,比較排序部分採用了氣泡排序。

程式碼:

#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;
}