【演算法學習】基於“平均”的隨機分配演算法(貪婪,回溯),以按平均工作量隨機分配單位為例
一、背景介紹
在工作中,遇到一個需求:將 N 個單位隨機分配給 n 個人,其中每個單位有對應的工作量,分配時要儘量按工作量平均分給 n 個人,且人員的所屬單位不能包括在被分配的單位中(N >= n)。例如:有三個部門分給兩個人([A]屬於部門2和[B]屬於部門3),部門1的工作量是999,部門2是2,部門3是4,這樣分配結果為 A分配部門3或部門1和部門3,B分配部門1和部門2或部門2。
二、演算法思路
剛開始的時候想不明白怎麼讓它們分配的“平均”,後面在網上找了找資料,於是面對這個需求我的大體演算法思路是:
1. 採用貪婪演算法將 N 個單位按照工作量進行分組;
1.1 對單位按工作量大小進行倒序排序;
1.2 首次分組時將最大工作量的 n 個先分組;
1.3 判斷是否有剩餘,n * i <= N?(i:分的次數),條件成立就執行 1.4,條件不成立就執行 1.5,一直迴圈直到所有單位分完;
1.4 接著第二次分組時找到分組的總工作量最小的,將單位分給總工作量最小的組;
1.5 將剩餘數量 N%n 個單位分配完。
2. 採用回溯演算法的思想將分組進行隨機分配給 n 個人。
2.1 迴圈對 n 個人進行分配,同時在分配時判定條件 2.2 、2.3 和 2.4,如果全部符合且每個人都分配完成,則分配完成。
2.2 判斷隨機出來的分組是否已分配,是:重新分配,否:繼續下一步;
2.3 判斷隨機出來的分組中單位是否包含了待分配人員的所屬單位,是:重新分配,否:繼續下一步;
2.4 判斷隨機了全部分組是否不能滿足 2.1 和 2.2 的條件,是:重新分配,否:繼續下一步;
三、原始碼
- 主程式
package com.select; import java.util.ArrayList; import java.util.List; /** * 主函式 * @author 歐陽 * @since 2018年11月7日 */ public class SelectMain { /** * 需求:將N個單位按工作量多少“儘量平均”分配給n個人,且所屬單位不能在分配的部門中,並且不論怎麼樣都必須有分配結果。 * 例如:有三個部門分給兩個人(屬於[A]部門2和[B]部門3),部門1的工作量是999,部門2是2,部門3是4, * 這樣分配結果為 A分配部門3或部門1和部門3,B分配部門1和部門2或部門2 * @param args */ public static void main(String[] args) { run(); } public static void run() { //1.初始化部門,人員資料 List<Department> departments = SelectUtil.initDept(); List<Person> persons = SelectUtil.initPerson(); //2.對部門進行從大到小排序 departments = SelectUtil.sortDesc(departments); //3.獲取人員所屬部門資料 List<Integer> personDepts = new ArrayList<>(); for(Person person : persons) { personDepts.add(person.getDeptNo()); } //4.按照工作量進行分組 List<Result> results = SelectUtil.group(departments, personDepts); //5.將分組結果隨機分配給每個人 results = SelectUtil.distribute(results, persons); System.out.println("+++++++++++ 部門資訊 +++++++++++"); Long workLoad = SelectUtil.workLoad(departments); System.out.println("總工作量:" + workLoad); System.out.println("平均工作量:" + workLoad/persons.size()); //6.在控制檯打印出分配結果 SelectUtil.print(results, persons); } }
- 工具類
package com.select; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Scanner; /** * 描述:工具類 * @author 歐陽 * @since 2018年11月7日 * @version 1.0 */ public class SelectUtil { /** * 描述:按照工作量進行分組 * @param departments 部門資料 * @param personDepts 人員所屬部門 * @return results 分組結果 * @author 歐陽榮濤 * @since 2018年11月6號 */ public static List<Result> group(List<Department> departments, List<Integer> personDepts) { List<Result> results = new ArrayList<>(); //儲存多個分組結果 Result result = new Result(); //記錄分組結果 int deptNum = departments.size(); //部門數量 int personNum = personDepts.size(); //人員數量 int num = deptNum / personNum; //分組次數 if(personNum % deptNum != 0) { num = num + 1; } //開始分組 int k = 0; //記錄下一個分組的部門,從0開始 for(int i=1; i<=num; i++) { if(i == 1) { //首次分組,分人員數量的組 for(int j=0; j<personNum; j++) { Department department = departments.get(k); result.addGroup(j, department); k++; // System.out.println(result.toString()); // System.out.println("下一個分組部門是:" + k); } } else { //分第二次到最後 /* * 貪婪,優先分給總工作量最小的 */ if(personNum * i <= deptNum) { for(int j=0; j<personNum; j++) { Department department = departments.get(k); Integer groupNo = findGroupMinLoad(result, personDepts, department); // System.out.println("找到最小工作量組:" + groupNo); result.addGroup(groupNo, department); k++; // System.out.println(result.toString()); // System.out.println("下一個分組部門是:" + k); } } else { //如果有剩餘,分最後剩餘的 for(int j=0; j<deptNum % personNum; j++) { Department department = departments.get(k); Integer groupNo = findGroupMinLoad(result, personDepts, department); // System.out.println("找到最小工作量組:" + groupNo); result.addGroup(groupNo, department); k++; // System.out.println(result.toString()); // System.out.println("下一個分組部門是:" + k); } } } } if(k == departments.size()) { results.add(result); } else { System.out.println("分組失敗!"); } return results; } /** * 描述:將分組結果隨機分給每個人,所屬單位不能包含在分組中 * @param results 分組結果(沒有分配結果) * @param persons 人員資訊 * @return results 分配結果 * @author 歐陽榮濤 * @since 2018年11月6號 */ public static List<Result> distribute(List<Result> results, List<Person> persons) { int number = persons.size(); //人員數量 int time = 0; //記錄分配次數 for(Result result : results) { Boolean flag = false; //分配成功標誌。預設是沒分配成功:false Map<Integer, List<Department>> groups = result.getResult(); //每個分組 Map<Integer,Integer> usedNum = new HashMap<>(); //記錄使用過的分組(隨機數) List<Integer> conDept = new ArrayList<>(); //記錄分組內的部門 while(!flag) { //隨機分配給每個人(回溯,只有分配條件成立才完成分配) System.out.println("正在隨機分配各組......"); time++; //分配次數加1 usedNum.clear(); for(Person person : persons) { conDept.clear(); //隨機生成組 int random = new Random().nextInt(number); //隨機數範圍:0<=random<number System.out.println("產生隨機數【" + random + "】"); System.out.println("嘗試將第【" + random + "】組進行分配......"); //判斷是否已經分配了,已經分配了就不能在分 if(usedNum.containsKey(random)) { System.out.println("第【" + random + "】組已分配,將重新分配......"); break; } //判斷分組是否包含所屬單位,包含就不能分 List<Department> departments = groups.get(random); Integer deptNo = person.getDeptNo(); //所屬單位 for(Department department : departments) { conDept.add(department.getDeptNo()); } if(conDept.contains(deptNo)) { System.out.println("第" + random + "組中包含所屬單位【" + deptNo + "】,將重新分配......"); break; } //判斷是否全部分配了也沒有找到分配結果需要重新分配 if(usedNum.size() == number) { System.out.println("分配方式出現問題,將重新分配......"); break; } //儲存已經分配的分組 usedNum.put(random, random); //分配分組 result.getToPerson().put(person.getPersonNo(), random); System.out.println("嘗試將第【" + random + "】組分配給【" + person.getName() + "】..."); } //分配成功,設定標誌,退出迴圈 if(usedNum.size() == number) { flag = true; break; } System.out.println("分配方案有誤,準備重新分配......"); } //分配成功,退出迴圈 if(flag) { System.out.println("【完成分配】,共分配【" + time + "】次"); break; } } return results; } /** * 描述:輸出分配結果 * @param results 分配結果 * @param persons 人員資訊 * @author 歐陽榮濤 * @since 2018年11月6號 */ public static void print(List<Result> results, List<Person> persons) { for(Result result : results) { Map<Integer, List<Department>> result2 = result.getResult(); //分組結果 Map<Integer, Integer> toPerson = result.getToPerson(); //分配結果 Map<Integer, Long> totalWorkLoad = result.getTotalWorkLoad(); //分組總工作量 System.out.println("+++++++++++ 分配結果 +++++++++++"); for(Integer personNo : toPerson.keySet()) { System.out.println("============================="); //查詢人員資訊 String personName = ""; //姓名 Integer deptNo = 0; //所屬部門 for(Person person : persons) { if(person.getPersonNo().equals(personNo)) { personName = person.getName(); deptNo = person.getDeptNo(); } } Integer groupNo = toPerson.get(personNo); System.out.println("【" + personName + "】," + "所屬部門:【 " + deptNo + "】," + "分配第【" + groupNo + "】組," + "包括有:"); for(Department department: result2.get(groupNo)) { System.out.println(department.getName() + "(" + department.getDeptNo() + ")" + ",工作量:" + department.getWorkLoad()); } System.out.println("總工作量:" + totalWorkLoad.get(groupNo)); } } } /** * 描述:找到分組中哪個分組的總的工作量最少 * @param result 分組結果 * @param personDepts 人員所屬部門 * @param department 待分組的部門 * @author 歐陽榮濤 * @return groupNo 工作量最小的分組的序號,如果每個人的所屬部門在同一組中返回倒數第二小的組 * @since 2018年11月7號 */ public static Integer findGroupMinLoad(Result result, List<Integer> personDepts, Department department) { Integer groupNo = 0; //預設最小的分組的序號為 0 Integer groupNoPre = 0; //記錄上一次最小的分組,預設為 0 Long minLoad = 99999L; //最小工作量,預設99999 //找最小工作量的組 Map<Integer, Long> totalWorkLoad = result.getTotalWorkLoad(); for(Integer key : totalWorkLoad.keySet()) { Long value = totalWorkLoad.get(key); if(value < minLoad) { minLoad = value; groupNo = key; } } //找最倒數第二小的組 Long poor = 99999L; //與最小工作量的差 for(Integer key : totalWorkLoad.keySet()) { Long value = totalWorkLoad.get(key); Long temp = Math.abs(value - minLoad); if(poor > temp && temp != 0) { groupNoPre = key; } } /* * 判斷最小分組中有多少個包含人員的所屬部門 */ Map<Integer, List<Department>> groupResult = result.getResult(); List<Department> list = groupResult.get(groupNo); int num = 0; //記錄分了幾個所屬部門在最小組 //1.先找出最小組中有幾個 for(Department dept : list) { if(personDepts.contains(dept.getDeptNo())) { num++; } } //2.再找待分組的部門是否也包含其中 if(personDepts.contains(department.getDeptNo())) { num++; } /* * 將要分組的部門如果分入最小組則會導致整組包含所有所屬部門,將不能分配, * 則將將要分組的部門分到倒數第二小組 */ if(num == personDepts.size()) { return groupNoPre; } return groupNo; } /** * 描述:將部門倒敘排序,並返回倒序結果 * @param departments 所有帶有工作量的部門 * @return departments 倒序後的結果 * @author 歐陽榮濤 * @since 2018年11月7號 */ public static List<Department> sortDesc(List<Department> departments) { Collections.sort(departments, new Comparator<Department>() { @Override public int compare(Department o1, Department o2) { if(o1.getWorkLoad() > o2.getWorkLoad()) { return -1; } else if(o1.getWorkLoad() == o2.getWorkLoad()) { return 0; } return 1; } }); return departments; } /** * 描述:計算總工作量 * @param departments 部門資訊 * @return total 總工作量 * @author 歐陽榮濤 * @since 2018年11月7號 */ public static Long workLoad(List<Department> departments) { Long total = 0L; for(Department department : departments) { total += department.getWorkLoad(); } return total; } /** * 描述:退出 * @author 歐陽 * @since 2018年11月7日 */ public static void exit() { Scanner sc = new Scanner(System.in); while(true) { System.out.println("按“0”退出程式"); String print = sc.next(); if("0".equals(print)) { sc.close(); break; } } System.exit(-1); } /** * 描述:初始化部門資訊 * @author 歐陽榮濤 * @since 2018年11月7日 */ public static List<Department> initDept() { List<Department> departments = new ArrayList<>(); //初始化部門 Department d1 = new Department(1, "市場部", 999L); Department d2 = new Department(2, "產品部", 18L); Department d3 = new Department(3, "開發部", 17L); Department d4 = new Department(4, "財物部", 16L); Department d5 = new Department(5, "專案部", 12L); Department d6 = new Department(6, "客服部", 14L); Department d7 = new Department(7, "運維部", 15L); departments.add(d1); departments.add(d2); departments.add(d3); departments.add(d4); departments.add(d5); departments.add(d6); departments.add(d7); return departments; } /** * 描述:初始化人員資訊 * @author 歐陽榮濤 * @since 2018年11月7日 */ public static List<Person> initPerson() { List<Person> persons = new ArrayList<>(); //初始化人員 Person p1 = new Person(1,"張三", 3); Person p2 = new Person(2,"李四", 4); Person p3 = new Person(3,"王五", 5); persons.add(p1); persons.add(p2); persons.add(p3); return persons; } }
- 將部門、人員和分組結果封裝成實體:
在分組結果實體中有個將部門新增到分組中,同時計算分組的總工作量的方法;
package com.select;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 描述:結果實體,用來存放分組結果
* @author 歐陽
* @since 2018年11月07日
* @version 1.0
*/
public class Result {
private Map<Integer, List<Department>> result; //儲存結果。key:組號,value:組成員
private Map<Integer, Long> totalWorkLoad; //儲存每組總的工作量
private Map<Integer, Integer> toPerson; //儲存每組分配的人員
public Result() {
super();
result = new HashMap<>();
totalWorkLoad = new HashMap<>();
toPerson = new HashMap<>();
}
/**
* 描述:將部門新增到分組中,同時計算分組的總工作量
* @param groupNo 新增的目標組號
* @param department 新增的目標部門
*/
public void addGroup(int groupNo, Department department) {
List<Department> list = result.get(groupNo);
if(list == null) {
list = new ArrayList<>();
}
//將新加的部門新增到分組
list.add(department);
result.put(groupNo, list);
//計算分組的總工作量
Long total = totalWorkLoad.get(groupNo);
if(total == null) {
totalWorkLoad.put(groupNo, department.getWorkLoad());
} else {
totalWorkLoad.put(groupNo, total+department.getWorkLoad());
}
}
public Map<Integer, List<Department>> getResult() {
return result;
}
public Map<Integer, Long> getTotalWorkLoad() {
return totalWorkLoad;
}
public Map<Integer, Integer> getToPerson() {
return toPerson;
}
@Override
public String toString() {
return "Result [result=" + result + ", totalWorkLoad=" + totalWorkLoad + ", toPerson=" + toPerson + "]";
}
}
部門實體:
package com.select;
/**
* 描述:部門實體,用來存放部門
* @author 歐陽
* @since 2018年11月07日
* @version 1.0
*/
public class Department {
private Integer deptNo; //部門號
private String name; //部門名稱
private Long workLoad; //工作量
public Department(Integer deptNo, String name, Long workLoad) {
super();
this.deptNo = deptNo;
this.name = name;
this.workLoad = workLoad;
}
public Integer getDeptNo() {
return deptNo;
}
public void setDeptNo(Integer deptNo) {
this.deptNo = deptNo;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getWorkLoad() {
return workLoad;
}
public void setWorkLoad(Long workLoad) {
this.workLoad = workLoad;
}
@Override
public String toString() {
return "Department [deptNo=" + deptNo + ", name=" + name + ", workLoad=" + workLoad + "]";
}
}
人員實體:
package com.select;
/**
* 描述:人員實體,用來存放人員
* @author 歐陽
* @since 2018年11月07日
* @version 1.0
*/
public class Person {
private Integer personNo; //人員編號
private String name; //人員姓名
private Integer deptNo; //所屬部門
public Person(Integer personNo, String name, Integer deptNo) {
super();
this.personNo = personNo;
this.name = name;
this.deptNo = deptNo;
}
public Integer getPersonNo() {
return personNo;
}
public void setPersonNo(Integer personNo) {
this.personNo = personNo;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getDeptNo() {
return deptNo;
}
public void setDeptNo(Integer deptNo) {
this.deptNo = deptNo;
}
@Override
public String toString() {
return "Person [personNo=" + personNo + ", name=" + name + ", deptNo=" + deptNo + "]";
}
}
四、遺留問題
1. 坐觀整個演算法,在分組時沒有考慮到多個分組結果,在最後的分組結果中還可以繼續對分組結果進行分配上的優化;希望有興趣的你給些寶貴意見和建議。
2. 在時間複雜度上感覺有點複雜。