【算法学习】基于“平均”的随机分配算法(贪婪,回溯),以按平均工作量随机分配单位为例

版权声明:转载请注明来源 https://blog.csdn.net/qq_24598601/article/details/83928514

一、背景介绍

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

猜你喜欢

转载自blog.csdn.net/qq_24598601/article/details/83928514