这个问题是《算法导论》上的一个经典的贪心算法问题——单处理器上具有期限和惩罚的单位时间任务调度问题,目标是使惩罚最小。
问题描述:
具有截止时间和误时惩罚的单位时间任务时间表问题可描述如下:
(1) n 个单位时间任务的集合S={1,2,…,n}(n≤500);
(2) 任务i的截止时间deadline[i],1≤d[i]≤n,即要求任务i在时间d[i]之前结束;
(3) 任务i 的误时惩罚1≤weught[i]<1000,即任务i 未在时间d[i]之前结束将招致w[i]的惩罚;若按时完成则无惩罚。
任务时间表问题要求确定S 的一个时间表(最优时间表)使得总误时惩罚达到最小。
实验要求:
(1)实现这个问题的贪心算法
(2)将每个 wi 替换为max{m1,m2…mn}—wi,运行算法比较结果。
输入与输出:
输入为任务的截止时间和任务未按时完成的惩罚
int[] deadline = new int[]{4,2,4,3,1,4,6}; // 任务期限,范围是闭区间[1,n]
int[] weight = new int[]{70,60,50,40,30,20,10}; // 任务未按时完成的惩罚
输出为最小的惩罚值、任务的具体调度顺序。
总的惩罚为:50
任务执行顺序为:
第1天执行的任务为: Task{id=3, deadline=3, weight=40}
第2天执行的任务为: Task{id=1, deadline=2, weight=60}
第3天执行的任务为: Task{id=2, deadline=4, weight=50}
第4天执行的任务为: Task{id=0, deadline=4, weight=70}
第5天执行的任务为: 任选一个已超时任务执行
第6天执行的任务为: Task{id=6, deadline=6, weight=10}
第7天执行的任务为: 任选一个已超时任务执行
思路分析:
主要参考了这篇文章任务调度问题(贪心思想)_还能坚持的博客-CSDN博客_任务调度问题
贪心思路:贪心考虑的是局部的最优解,而不是全局的最优解,为了达到最小的误时惩罚,按照贪心策略,肯定是先去按时完成或提前完成惩罚较大的任务。若一个时间片上已经安排了按时完成的、有着较大惩罚的任务,那么,这个有着较小惩罚的任务,则需要提前完成(提前完成有着很多种方法,按照贪心策略,应该是放在紧邻的上一个时间片完成,若这个时间片也有任务,继续往前移,直到有空闲的时间片可以执行这个任务,否则,这个任务会带来惩罚)。
解题步骤1:根据罚时的大小进行排序,将惩罚大的放在前面。开一个数组route作为时间槽,记录每个单位时间是否有任务安排。尽量将任务安排在deadline当天完成,若当天已有任务则安排在前面最近的某一天。若在第deadline天及之前都有任务安排,先不要将该任务加入到时间槽中(可能产生后效性),可以将这个任务舍去,同时,将其增加到罚时中,等到对全部的任务过滤一遍后,可以将罚时的任务随意地插入到时间槽中 (既然都有罚时了,那么在截止时间后的任何一个时间片,完成该任务都一样)。
具体实现:
网上找了一圈,这道题基本上都是用cpp或python写的, 故来造一个java版本的轮子。
①任务结点定义:
public class Task implements Comparable<Task>{
public int id;
public int deadline;
public int weight;
public Task(int id, int deadline, int weight) {
this.id = id;
this.deadline = deadline;
this.weight = weight;
}
@Override
public String toString() {
return "Task{" +
"id=" + id +
", deadline=" + deadline +
", weight=" + weight +
'}';
}
@Override
public int compareTo(Task newTask) { // 按惩罚由大到小排序,调用Arrays.sort时会用到
return Integer.compare(newTask.weight, this.weight);
}
②贪心思想实现任务调度算法
import java.util.Arrays;
// 在单处理器上具有期限和惩罚的单位时间任务调度问题。
public class TaskDispatch {
private static void greedyTaskDispatch(Task[] tasks) {
int n = tasks.length;
Arrays.sort(tasks); // 根据惩罚从大到小排序
System.out.println("Task按惩罚从大到小排序后:");
for(Task t : tasks) {
System.out.println(t.toString());
}
int[] route = new int[n]; // 记录任务的最终调度顺序,route[i]表示第(i+1)天调度的任务id。天数从1开始,而数组下标从0开始。
Arrays.fill(route, -1);
int punishment = 0; // 记录总的惩罚值
for(int i = 0; i < n; ++i) { // 处理第i个任务
for(int j = tasks[i].deadline - 1; j >= 0; --j) { // deadline的范围是[1,n],故此处需修正下标
// j为第i个任务的deadline,应将第i个任务尽可能在靠近deadline的天完成。
// 这样卡点完成的好处是:让后续有更紧急deadline要求的任务更可能按时完成。
if(route[j] == -1) { // 若第j天未安排任务
route[j] = tasks[i].id;
break;
}
if(j == 0) { // 第1天~第deadline天都已安排了任务,故当前任务必超时
punishment += tasks[i].weight;
}
}
}
System.out.println("总的惩罚为:" + punishment);
System.out.println("任务执行顺序为:");
for(int i = 0; i < n; ++i) {
System.out.print("第" + (i+1) + "天执行的任务为:");
if(route[i] != -1) {
System.out.println(" " + tasks[route[i]]);
}
else {
System.out.println(" 任选一个已超时任务执行");
}
}
}
public static void main(String[] args) {
int n = 7; // 任务数量
int[] deadline = new int[]{4,2,4,3,1,4,6}; // 任务期限,范围是闭区间[1,n]
int[] weight = new int[]{70,60,50,40,30,20,10}; // 任务未按时完成的惩罚
// 将 Wi 替换为 max{W1,W2……Wn} - Wi
// int maxWeight = weight[0];
// for(int w : weight) {
// maxWeight = w > maxWeight ? w: maxWeight;
// }
// for(int i = 0; i < n; ++i) {
// weight[i] = maxWeight - weight[i];
// }
Task[] tasks = new Task[n];
for(int i = 0; i < n; ++i) {
tasks[i] = new Task(i, deadline[i], weight[i]);
}
greedyTaskDispatch(tasks);
}
}