一、背景介绍

在工作中,遇到一个需求:将 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 departments = SelectUtil.initDept();

List persons = SelectUtil.initPerson();

//2.对部门进行从大到小排序

departments = SelectUtil.sortDesc(departments);

//3.获取人员所属部门数据

List personDepts = new ArrayList<>();

for(Person person : persons) {

personDepts.add(person.getDeptNo());

}

//4.按照工作量进行分组

List 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 group(List departments, List personDepts) {

List 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

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

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

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 distribute(List results, List persons) {

int number = persons.size(); //人员数量

int time = 0; //记录分配次数

for(Result result : results) {

Boolean flag = false; //分配成功标志。默认是没分配成功:false

Map> groups = result.getResult(); //每个分组

Map usedNum = new HashMap<>(); //记录使用过的分组(随机数)

List 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

System.out.println("产生随机数【" + random + "】");

System.out.println("尝试将第【" + random + "】组进行分配......");

//判断是否已经分配了,已经分配了就不能在分

if(usedNum.containsKey(random)) {

System.out.println("第【" + random + "】组已分配,将重新分配......");

break;

}

//判断分组是否包含所属单位,包含就不能分

List 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 results, List persons) {

for(Result result : results) {

Map> result2 = result.getResult(); //分组结果

Map toPerson = result.getToPerson(); //分配结果

Map 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 personDepts,

Department department) {

Integer groupNo = 0; //默认最小的分组的序号为 0

Integer groupNoPre = 0; //记录上一次最小的分组,默认为 0

Long minLoad = 99999L; //最小工作量,默认99999

//找最小工作量的组

Map 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> groupResult = result.getResult();

List 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 sortDesc(List departments) {

Collections.sort(departments, new Comparator() {

@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 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 initDept() {

List 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 initPerson() {

List 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> result; //存储结果。key:组号,value:组成员

private Map totalWorkLoad; //存储每组总的工作量

private Map 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 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> getResult() {

return result;

}

public Map getTotalWorkLoad() {

return totalWorkLoad;

}

public Map 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. 在时间复杂度上感觉有点复杂。