1. 程式人生 > >【演算法學習】基於“平均”的隨機分配演算法(貪婪,回溯),以按平均工作量隨機分配單位為例

【演算法學習】基於“平均”的隨機分配演算法(貪婪,回溯),以按平均工作量隨機分配單位為例

一、背景介紹

  在工作中,遇到一個需求:將 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 的條件,是:重新分配,否:繼續下一步;

三、原始碼

  1. 主程式
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);
	}

}

  1. 工具類
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;
		
	}
}



  1. 將部門、人員和分組結果封裝成實體:
    在分組結果實體中有個將部門新增到分組中,同時計算分組的總工作量的方法;
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. 在時間複雜度上感覺有點複雜。