【演算法】基於優先順序的排班演算法實現
場景:在大學的裡,有不少社團組織會要組織中的成員值班,當然這個值班時間是學生無課的時間才會被安排值班。
假設現有如下需求:一天中有3個時間段要有人值班,每週週一到週五都要值班,就是共有15個值班段,每個時間段值班的人都不一樣,現有40個學生,要求根據這些學生的無課表情況安排值班,要求每個值班段必須有兩個人,每個人一週只值班一次(如果某一值班段只有一人無課,那該值班段就只能一人值班)。
小插一題,做一排列組合題:
有10個相同的糖果,頒給3個小人,要求每個人至少有2個糖果,問共有多少種分法?
(自行解答,2014年阿里實習生招有個這樣的題:有10個相同的糖果,頒給3個小人,要求每個人至少有1個糖果,問共有多少種分法?)
排班演算法設計的難點主要在於每個人的無課表都不一樣,如果像上面的題那樣的,不用根據每個人的無課表,每個人在任意一值班段都能值班,那就簡單啦。
以下講講個人演算法的設計:
1. 基於回溯法的遍歷:(實現思路:當安排到第N個值班段時,發現沒有滿足條件的可以選,就回溯到第N-1個值班段重新安排,如果第N-1個值班段的人都試過但第N個值班段還是沒有滿足條件的可以選,就回溯到第N-2個值班段,以此類推不斷回溯)
現將上面的問題資料量縮小下來分析下這個問題:
假設有三個值班段,三個人,根據這三個人的無課表情況給值班段安排人值班。
思路:
先在第一個值班段無課的人中選擇一個人安排到該值班段值班,被安排過的人就被標誌為被安排值班(flag為0表示未被安排,flag為1表示已被安排),就不能再被安排到其他值班段值班了,以此類推,安排完一個值班段就安排下一個值班段。
最好情況:
最好情況下就是安排到每個值班段都有人未被安排過值班的人,這樣一次性所有就排完了,如下圖:
(1、2、3表示值班段,每個值班段裡的字母表示該值班段無課的人,如上面第1個值班段有A和B兩個人無課,可以安排在該值班段值班)
A被安排到1值班段,
B被安排到2值班段,
C被安排到3值班段。
最壞情況:
如下圖所示:
安排到第三個值班段時因為A已經被安排過值班,所以要回溯到第二個值班段重新安排,就給第二個值班段安排C值班,但此時第三個值班段還是沒人可以值班,而第二個值班段的人也都遍歷過了,所以此時就要回溯到第一個值班段重新安排,此時:
B被安排到1值班段,
C被安排到2值班段,
A被安排到3值班段。
2. 基於優先順序的選擇:(實現思路:安排值班時不是從第一個值班段順序地安排到第十五個值班段,因為在15個值班段中,有些值班段無課的人比較多,此時的選擇就比較多,而有此值班段無課的人很少,那麼這個值班就要優先安排人值班,比如有個值班段只有兩人無課,那就要先安排這兩個人在該值班段值班,這是第一個優先順序;每個人一週內無課的次數都不一樣,有些人一週只有一兩次無課的時間,而有些人一週有五六次無課的時間,在一個值班段選擇誰在該值班段值班時優先選擇無課次數少的,這是第二個優先順序)
<span style="font-family:SimSun;font-size:14px;">// 值班段類
public class DutyTime {
private String time; // 值班時間
private List<User> freeUsers = new ArrayList<User>(); // 該時間段無課的人
private List<User> dutyUsers = new ArrayList<User>(); // 該時間段值班的人員
public DutyTime(){}
public DutyTime(String time) {
super();
this.time = time;
}
// 省略get set
}
</span>
<span style="font-family:SimSun;font-size:14px;">// 人員
public class User {
private String name;
private int num; // 該人一週無課時間段數
public User(){}
public User(String name) {
super();
this.name = name;
}
<span style="white-space:pre"> </span>// 省略get set
}
</span>
<span style="font-family:SimSun;font-size:14px;">/**
* 值班安排:
* 先為每個值班段安排一名人員值班(第一輪),安排一輪後再進行第二輪、第三輪
* 按優先順序安排值班:
* 1.各個值班段可值班人員數少的優先安排值班
* 2.在一個值班段選擇人員時,優先選擇總無課次數少的人
* 注:人員在被安排後就會從該值班段中的無課人員集合中刪除,這樣之後就不用再遍歷到;在一輪安排後開始下一輪時,每個值班段的排人順序會重新調整(因為去掉了已被安排的人)
*
*/
public class Schedule3 {
public static void main(String[] args) {
User A = new User("A", 4); // A一週有4個時間段無課
...
DutyTime time1 = new DutyTime("1"); // 第一個值班段
...
time1.getFreeUsers().add(A);
...
List<DutyTime> dutyTimeList = new ArrayList<>();
dutyTimeList.add(time1);
...
for (int w = 0; w < 3; w++) {
// 排序,可用於值班的人數少的值班段排在前面,下面安排值班是可用於值班的人數少的值班段優先安排
dutyTimeList = dutyTimeSort(dutyTimeList);
// 遍歷已排好序的值班段,為每個值班段安排值班人員
for (int i = 0; i < dutyTimeList.size(); i++) {
// 如果該值班段有無課的人才安排值班
if (dutyTimeList.get(i).getFreeUsers().size() > 0) {
// 得到的都是未被安排過值班的人,並排好序,總無課次數少的人排前面
List<User> freeUserList = freeUserSort(dutyTimeList.get(i).getFreeUsers());
// 將可在該值班段值班的人安排到該值班段值班
dutyTimeList.get(i).getDutyUsers().add(freeUserList.get(0));
System.out.println(freeUserList.get(0).getName() + " 被安排到" + dutyTimeList.get(i).getTime() + "值班;該值班段現有"+dutyTimeList.get(i).getDutyUsers().size()+"人值班,"+freeUserList.get(0).getName()+"一週總無課次數為:"+freeUserList.get(0).getNum());
// 將被安排的人從各個值班段(有該人的值班段)中刪除,下次不用再遍歷
User delUser = freeUserList.get(0);
for (int j = 0; j < dutyTimeList.size(); j++) {
if (dutyTimeList.get(j).getFreeUsers().contains(delUser)) {
dutyTimeList.get(j).getFreeUsers().remove(delUser);
System.out.println("---"+delUser.getName()+"已從"+dutyTimeList.get(j).getTime()+"值班段刪除,目前該值班段剩餘未被安排人數有:"+dutyTimeList.get(j).getFreeUsers().size());
}
}
}else {
System.out.println(dutyTimeList.get(i).getTime()+"該值班段已無人可安排值班");
}
}
System.out.println("---------------------安排了一輪--------------------");
}
}
// 對某一值班段中無課的人進行排序(一週總無課次數少的人排在前面)
public static List<User> freeUserSort(List<User> freeUserList);
// 對值班段排序(可用於值班的人數少的值班段排在前面)
public static List<DutyTime> dutyTimeSort(List<DutyTime> dutyTimeList);
}</span>
使用這種方法實現排班只能生成一張值班表,但現實中根據總的無課表是可以排出不同方案的值班表的,以上的演算法小編只是在滿足個人專案需求的情況下設計的,並不是通用的,小編查過許多高校的教授發表的關於排課演算法實現的論文,那些情況就更加複雜了,各種回溯各種優先順序。
排課問題在70年代就證明是一個NP完全問題,即演算法的計算時間是呈指數增長的,這一論斷確立了排課問題的理論深度。對於NP問題完全問題目前在數學上是沒有一個通用的演算法能夠很好地解決。
Author:顧故
Sign:別輸給曾經的自己