题目:https://leetcode-cn.com/problems/find-minimum-time-to-finish-all-jobs/

给你一个整数数组 jobs ,其中 jobs[i] 是完成第 i 项工作要花费的时间。请你将这些工作分配给 k 位工人。所有工作都应该分配给工人,且每项工作只能分配给一位工人。工人的 工作时间 是完成分配给他们的所有工作花费时间的总和。请你设计一套最佳的工作分配方案,使工人的 最大工作时间 得以 最小化 。返回分配方案中尽可能 最小 的 最大工作时间 。
示例 1:
输入:jobs = [3,2,3], k = 3  输出:3
解释:给每位工人分配一项工作,最大工作时间是 3 。

示例 2:
输入:jobs = [1,2,4,7,8], k = 2 输出:11
解释:按下述方式分配工作,最大工作时间是 11 。
      1 号工人:1、2、8(工作时间 = 1 + 2 + 8 = 11)
      2 号工人:4、7(工作时间 = 4 + 7 = 11)

提交结果:

工时分配 python 工时分配方案_工作最小时间

 

解题思路:
1、将工作排序,优先分配【耗时大的工作】给【当前工时最少的工人】,得出初步方案maxTime。
   参考方法 public static JobAllotDetails getJobAllotDetails(JobAllotDetails jobDetail, int way);
2、找比maxTime更小的工作分配方案,有两套方案
  1) 使用递归回溯+剪枝算法,尝试找到比maxTime更小的方案,参考方法dfs()
  2) 可以知道最大工作量在【工人平均耗时minJobTime】~【初步分配最大耗时maxTime】之间。
     用二分查找算法每次取minJobTime~maxTime中间数findJobTime,是否能找到分配方案。
     如果能,说明可能还有小的方案maxTime=findJobTime。如果不能minJobTime=findJobTime
     循环执行直到findJobTime>=maxTime

以下为不考虑内存,容易调试的代码,最终打印找到的方案:

/** */
package com.wjz.study.algorithm.allot;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 工作份配
 * https://leetcode-cn.com/problems/find-minimum-time-to-finish-all-jobs/
 * @author: 王金洲 2021-02-23 16:38:37
 */
public class JobAllot {
	protected static Logger logger = LoggerFactory.getLogger(JobAllot.class);

	public static void main(String[] args) {
		List<JobAllotDetails> data = new ArrayList<>();
		data.add(new JobAllotDetails(3, 3, new int[] { 3, 2, 3 }));
		data.add(new JobAllotDetails(2, 11, new int[] { 1, 2, 4, 7, 8 }));
		data.add(new JobAllotDetails(3, 29, new int[] { 12, 13, 14, 17, 25 }));
		data.add(new JobAllotDetails(2, 12, new int[] { 5, 4, 5, 4, 4 }));
		data.add(new JobAllotDetails(9, 20, new int[] { 11, 2, 20, 18, 2, 1, 7, 11, 7, 10 }));
		data.add(new JobAllotDetails(9, 9899456,
			new int[] { 9899456, 8291115, 9477657, 9288480, 5146275, 7697968, 8573153, 3582365, 3758448, 9881935, 2420271, 4542202 }));
		data.add(new JobAllotDetails(10, 501, new int[] { 250, 250, 256, 251, 254, 254, 251, 255, 250, 252, 254, 255 }));
		data.add(new JobAllotDetails(4, 16274131,
			new int[] { 6518448, 8819833, 7991995, 7454298, 2087579, 380625, 4031400, 2905811, 4901241, 8480231, 7750692, 3544254 }));

		for (JobAllotDetails jobDetail : data) {
			jobDetail = getJobAllotDetails(jobDetail, 1);
			if (jobDetail.maxTime != jobDetail.rightMaxTime) {
				throw new RuntimeException("最大工时不正确maxTime=" + jobDetail.maxTime + ",rightMaxTime=" + jobDetail.rightMaxTime);
			}
		}
	}

	/**
	 * 给你一个整数数组 jobs ,其中 jobs[i] 是完成第 i 项工作要花费的时间。
	 * 请你将这些工作分配给 k 位工人。所有工作都应该分配给工人,且每项工作只能分配给一位工人。工人的 工作时间 是完成分配给他们的所有工作花费时间的总和。请你设计一套最佳的工作分配方案,使工人的 最大工作时间 得以 最小化 。
	 * 来源:力扣(LeetCode)
	 * 链接:https://leetcode-cn.com/problems/find-minimum-time-to-finish-all-jobs
	 * @param jobDetail
	 * @return
	 * @author: 王金洲 2021-05-10 16:22:13
	 */
	public static JobAllotDetails getJobAllotDetails(JobAllotDetails jobDetail, int way) {
		// 对工作排序,初始工人信息
		Arrays.sort(jobDetail.jobs);
		jobDetail.initWorkerJobMap();

		// 初步分配,从大到小依次分配给工人
		int jobTotalTime = 0;// 工作总耗时
		for (int i = jobDetail.jobs.length - 1; i >= 0; i--) {
			// 找到当前工作时长最少的工人
			WorkerJob minTimeWorker = jobDetail.workerJobMap.get(0);
			for (int j = 1; j < jobDetail.workerJobMap.size(); j++) {
				if (minTimeWorker.totalTime > jobDetail.workerJobMap.get(j).totalTime) {
					minTimeWorker = jobDetail.workerJobMap.get(j);
				}
			}
			// 给【时长最少的工人】分配工作
			jobTotalTime += jobDetail.jobs[i];
			minTimeWorker.totalTime += jobDetail.jobs[i];
			minTimeWorker.jobs.add(jobDetail.jobs[i]);
			if (jobDetail.maxTime < minTimeWorker.totalTime) {
				jobDetail.maxTime = minTimeWorker.totalTime;
			}
		}
		// 一人一份工作
		if (jobDetail.jobs.length <= jobDetail.workerCount) {
			printDetail(jobDetail, false, "【最终方案】");
			return jobDetail;
		}
		printDetail(jobDetail, true, "【初步分配】");

		// 初步分配后,取得工人最大时间,并不是最优方案,采用【回溯算法】进行调优
		if (way == 0) {
			// 尝试找到比初步分配方案更少的方案
			dfs(jobDetail, jobDetail.jobs.length - 1, 0);
		} else {
			// 初步分配后,最大工作量在【工人平均耗时】~【初步分配最大耗时】之间
			int minJobTime = ceilDiv(jobTotalTime, jobDetail.workerCount).intValue();
			int findJobTime = minJobTime + floorDiv(jobDetail.maxTime - minJobTime, 2).intValue();// max=13,min=12,查找应该从12开始
			int currMaxTime = jobDetail.maxTime;
			while (findJobTime < jobDetail.maxTime) {
				logger.info("在{}~{}间【查找】最优的最大工时:{},上次获得最大工时为:{}", minJobTime, currMaxTime, findJobTime, jobDetail.maxTime);
				if (tryAllotJob(jobDetail, jobDetail.jobs.length - 1, findJobTime)) {
					// 如果能分配工作,说明还有可能找到更小的组合
					for (int i = 0; i < jobDetail.workerCount; i++) {
						jobDetail.workerJobTmpMap.put(i, new WorkerJob(i, new ArrayList<Integer>()));
					}
					findJobTime = minJobTime + floorDiv(jobDetail.maxTime - minJobTime, 2).intValue();// 二分查找,max=13,min=12,查找应该从12开始
					currMaxTime = jobDetail.maxTime;
				} else {
					minJobTime = findJobTime;
					findJobTime = findJobTime + ceilDiv(jobDetail.maxTime - minJobTime, 2).intValue();// 二分查找,max=13,min=12,查找应该从13开始
				}
			}
		}
		printDetail(jobDetail, false, "【最终方案】");
		return jobDetail;
	}

	/**
	 * 【递归回溯算法】尝试分配工作
	 * @param jobDetail
	 * @param currJobIndex
	 * @param limitTime
	 * @author: 王金洲 2021-05-11 13:23:21
	 */
	public static boolean tryAllotJob(JobAllotDetails jobDetail, int currJobIndex, int limitTime) {
		// 任务刚好成功分配完了
		if (currJobIndex < 0) {
			jobDetail.maxTime = limitTime;
			jobDetail.copyWorerJobMap();
			// printDetail(jobDetail, false, "【新方案】");
			return true;
		}

		WorkerJob currWorker = null;
		for (int i = jobDetail.workerJobTmpMap.size() - 1; i >= 0; i--) {
			// 当前工人未超过限制
			currWorker = jobDetail.workerJobTmpMap.get(i);
			if (currWorker.totalTime + jobDetail.jobs[currJobIndex] <= limitTime) {
				// 递归后回溯
				currWorker.totalTime += jobDetail.jobs[currJobIndex];
				currWorker.jobs.add(jobDetail.jobs[currJobIndex]);
				if (tryAllotJob(jobDetail, currJobIndex - 1, limitTime)) {
					return true;
				}
				currWorker.totalTime -= jobDetail.jobs[currJobIndex];
				currWorker.jobs.remove(currWorker.jobs.size() - 1);
			}

			// 要保证每一个人都有工作,所以当前工人没有可以安排的工作,结束
			if (jobDetail.workerJobTmpMap.get(i).totalTime == 0) {
				break;
			}

		}
		return false;
	}

	/**
	 * 【递归回溯算法】尝试找到比初步分配方案更少的方案
	 * @param jobDetail
	 * @param currJobIndex
	 * @param currMaxTime
	 * @author: 王金洲 2021-05-11 13:23:21
	 */
	public static void dfs(JobAllotDetails jobDetail, int currJobIndex, int currMaxTime) {
		// 任务刚好成功分配完了
		if (currJobIndex < 0) {
			if (currMaxTime < jobDetail.maxTime) {
				jobDetail.maxTime = currMaxTime;
				jobDetail.copyWorerJobMap();
				printDetail(jobDetail, false, "【新方案】");
			} else {
				// printDetail(jobDetail, false, "【可行方案】");
			}
			return;
		}

		WorkerJob currWorker = null;
		for (int i = jobDetail.workerJobTmpMap.size() - 1; i >= 0; i--) {
			// 当前工人总工时超过限制了,跳过
			currWorker = jobDetail.workerJobTmpMap.get(i);
			if (currWorker.totalTime + jobDetail.jobs[currJobIndex] < jobDetail.maxTime) {
				// 递归后回溯
				currWorker.totalTime += jobDetail.jobs[currJobIndex];
				currWorker.jobs.add(jobDetail.jobs[currJobIndex]);
				dfs(jobDetail, currJobIndex - 1, Math.max(currWorker.totalTime, currMaxTime));
				currWorker.totalTime -= jobDetail.jobs[currJobIndex];
				currWorker.jobs.remove(currWorker.jobs.size() - 1);
			}

			// 要保证每一个人都有工作,所以当前工人没有可以安排的工作,结束
			if (jobDetail.workerJobTmpMap.get(i).totalTime == 0) {
				break;
			}
		}
	}

	/**
	 * 打印对象
	 * @param jobDetail
	 * @param showAllotJob
	 * @param msgs
	 * @author: 王金洲 2021-05-11 10:03:45
	 */
	public static void printDetail(JobAllotDetails jobDetail, boolean showAllotJob, String... msgs) {
		String msg = "";
		if (msgs != null && msgs.length > 0) {
			msg = msgs[0];
		}

		if (showAllotJob) {
			logger.info("========== 待分配工作:工人数={},工作={} =======", jobDetail.workerCount, jobDetail.jobs);
		}
		logger.info("== {}最大耗时={}", msg, jobDetail.maxTime);
		for (WorkerJob d : jobDetail.workerJobMap.values()) {
			logger.info("   工人{},总时长={},工作内容={}", d.workerIndex, d.totalTime, d.jobs);
		}
		logger.info("\r\n");
	}

	/**
	 * 工作分配详情
	 * @author: 王金洲 2021-05-10 13:38:52
	 */
	public static class JobAllotDetails {
		/** 工人数 */
		public int workerCount;
		/** 待分配工作及耗时 */
		public int[] jobs;
		/** 最大耗时 */
		public int maxTime;
		/** 最大耗时-正确值 */
		public int rightMaxTime;
		/** 每个工人的工作详情 */
		public Map<Integer, WorkerJob> workerJobMap;
		/** 每个工人的工作详情-临时方案 */
		public Map<Integer, WorkerJob> workerJobTmpMap;

		/**
		 * 构造函数
		 * @param workerCount
		 * @param jobs
		 */
		public JobAllotDetails(int workerCount, int rightMaxTime, int[] jobs) {
			super();
			this.workerCount = workerCount;
			this.rightMaxTime = rightMaxTime;
			this.jobs = jobs;
		}

		/**
		 * 初始化工人工作清单
		 * @author: 王金洲 2021-05-10 17:06:53
		 */
		public void initWorkerJobMap() {
			workerJobMap = new HashMap<>();
			workerJobTmpMap = new HashMap<>();
			for (int i = 0; i < workerCount; i++) {
				workerJobMap.put(i, new WorkerJob(i, new ArrayList<Integer>()));
				workerJobTmpMap.put(i, new WorkerJob(i, new ArrayList<Integer>()));
			}
		}

		/**
		 * 复制临时方案为最终方案
		 * @author: 王金洲 2021-05-10 17:06:38
		 */
		public void copyWorerJobMap() {
			int maxTime = 0;
			for (int i = 0; i < workerCount; i++) {
				WorkerJob tmp = workerJobTmpMap.get(i);
				if (maxTime < tmp.totalTime) {
					maxTime = tmp.totalTime;
				}
				workerJobMap.put(i, new WorkerJob(i, tmp.totalTime, new ArrayList<Integer>(tmp.jobs)));
			}
			this.maxTime = maxTime;
		}

		/**
		 * @return
		 * @author: 王金洲 2021-05-10 17:18:45
		 */
		@Override
		public String toString() {
			StringBuffer sb = new StringBuffer();
			sb.append("========== 待分配工作:工人数=" + workerCount + ",工作=" + jobs.toString() + " =======\r\n");
			sb.append("== 最大耗时=" + maxTime + "\r\n");
			for (WorkerJob d : workerJobMap.values()) {
				sb.append(d.toString() + "\r\n");
			}
			return sb.toString();
		}

		public String toString2() {
			StringBuffer sb = new StringBuffer();
			sb.append("========== 待分配工作:工人数=" + workerCount + ",工作=" + jobs.toString() + " =======\r\n");
			sb.append("== 最大耗时=" + maxTime + "\r\n");
			for (WorkerJob d : workerJobTmpMap.values()) {
				sb.append(d.toString() + "\r\n");
			}
			return sb.toString();
		}
	}

	/**
	 * 每个工人的工作信息
	 * @author: 王金洲 2021-05-10 13:38:52
	 */
	public static class WorkerJob {
		/** 工人编号 */
		public int workerIndex;
		/** 当前工作总耗时 */
		public int totalTime;
		/** 工作清单 */
		public List<Integer> jobs;

		/**
		 * 构造函数
		 * @param workerIndex
		 * @param jobs
		 */
		public WorkerJob(int workerIndex, List<Integer> jobs) {
			super();
			this.workerIndex = workerIndex;
			this.jobs = jobs;
		}

		/**
		 * 构造函数
		 * @param workerIndex
		 * @param totalTime
		 * @param jobs
		 */
		public WorkerJob(int workerIndex, int totalTime, List<Integer> jobs) {
			super();
			this.workerIndex = workerIndex;
			this.totalTime = totalTime;
			this.jobs = jobs;
		}

		/**
		 * @return
		 * @author: 王金洲 2021-05-10 17:16:21
		 */
		@Override
		public String toString() {
			return "工人" + workerIndex + ": 总时长=" + totalTime + ", 工作内容=" + jobs.toString();
		}
	}

	/**
	 * ab除以b后,向上取整,如3/2=1.5,向上取整=2
	 * @param a
	 * @param b
	 * @return
	 * @author: 王金洲 2021-05-12 13:29:34
	 */
	public static <T extends Number> Long ceilDiv(T a, T b) {
		return new BigDecimal(a.toString()).divide(new BigDecimal(b.toString()), RoundingMode.CEILING).longValue();
	}

	/**
	 * ab除以b后,向下取整,如3/2=1.5,向下取整=1
	 * @param a
	 * @param b
	 * @return
	 * @author: 王金洲 2021-05-12 13:29:34
	 */
	public static <T extends Number> Long floorDiv(T a, T b) {
		return new BigDecimal(a.toString()).divide(new BigDecimal(b.toString()), RoundingMode.FLOOR).longValue();
	}

}