一、背景介绍
在工作中,遇到一个需求:将 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. 在时间复杂度上感觉有点复杂。