5.1 题目描述
区间调度问题:有n项工作,每项工作分别在si开始,fi结束。对每项工作,你都可以选择参加或不参加,但选择了参加某项工作就必须至始至终参加,全程参与,即参与工作的时间段不能有重叠(即使开始的时间和结束的时间重叠都不行)。
这个问题在贪心算法一节习题中出现过。原来的目标是使得你能参与的工作数量最多。但是现在我们对每个工作赋予一个权值,它可能表示这项工作你能获得的报酬。请问,你应该选择哪些工作,才能使得你获得的报酬最多。
这个问题也被称为区间调度问题。如图2.4,按照贪心策略,先后选择活动1和活动3,它们是相容的,但是你能获得的总报酬为2,没有只从事工作2获得的报酬多。请你设计算法,帮助我们选择最优的活动方案。
图2.4 任务分配
5.2 程序使用说明
- 输入任务个数n;
2. 依次输入数据每个任务的开始时间tasks[i][0]、结束时间tasks[i][1]、价值tasks[i][2];
3. 输入任务最终结束时间lastTime。5.3 分析和设计
- 思路
对于第i个任务有两种选择方案,即做与不做,如果选择做第i个任务,那么获得的最大价值就是当前任务价值加上前i-1个任务在第i个任务开始之前所获取的最大值,而如果选择不做第i个任务,那么获得的最大价值就是前i-1个任务在第i个任务结束的时间条件下获得的最大价值。为了获得最优解,需要对两种方案进行比较,也就是说需要知道前i-1个物品在不同时间下可获得的最大价值,所以有递推公式F(i, t) = max(F(i-1, t), F(i-1, tasks[i][0])+tasks[i][2]),当然如果时间t没有超过任务结束时间,那么该任务就不能选择,F(i, t) = F(i-1, t)。运用动态规划的思想,自顶向下依次求解第i个物品在时间轴的不同选择获得的最大价值并记录到数组中。数组的最后一个值就是就是获得的最大价值。
2. 伪代码ChooseTask(tasks[1...n, 0...2], lastTime)
// 输入:任务信息tasks,tasks[i][0]、tasks[i][1]、tasks[i][2]分别表示// 任务的开发时间、结束时间、价值,任务的最后结束时间lastTime
// 输出:路径
maxValue[0...n][0...lastTime] <- 0
// 自顶向下依次计算每个任务在时间轴上的安排所获得的最大值
for i <- 1 to n do
for j <- 1 to lastTime do
if j < task[i, 1]
// j时,任务未结束
maxValue[i, j] <- maxValue[i-1, j]
else
maxValue[i, j] <- max(maxValue[i-1,j], tasks[i, 2]+maxValue[i-1, tasks[i, 0]])
// 逆推出选择方案
choose[1...n] <- 0
nowTime <- lastTime
for i <- n to 1 do
if maxValue[i, nowTime] = maxValue[i-1, nowTime]
// 没有选择任务i
choose[i] <- 0
else
choose[i] <- 1
nowTime <- tasks[i, 0]
return choose
- 时间复杂度
在动态规划中,时间复杂度就是把数组填充完成的时间,所以时间复杂度为O(n*leastTime)。5.4 测试用例
| | | | | | | —- | —- | —- | —- | —- | | 1 | n = 3
tasks = [[1, 3, 2],
[4, 6, 4],
[7, 9, 1]]
lastTime = 9 | 7 | 7 | 通过 | | 2 | n = 3
tasks = [[4, 5, 2],
[4, 6, 4],
[7, 9, 1]]
lastTime = 9 | 5 | 5 | 通过 | | 3 | n = 3
tasks= [[3, 5, 2],
[2, 6, 4],
[5, 9, 1]]
lastTime = 9 | 4 | 4 | 通过 |
5.5 源代码(含注释)
import java.util.Scanner;
public class TaskManage {
public static void main(String[] args) {
int[][] tasks = new int[][]{{0, 0, 0},{3, 5, 2},{4, 6, 4},{5, 9, 1}};
int leastTime = 9;
System.out.println("案例\n3 5 2\n2 6 4\n5 9 1");
fillValue(tasks, leastTime);
Scanner scanner = new Scanner(System.in);
System.out.println();
System.out.print("请输入任务个数:");
int n = scanner.nextInt();
tasks = new int[n+1][3];
for (int i = 1; i <= n; i++){
System.out.print("请输入任务"+i+"起始时间、终止时间、报酬:");
tasks[i] = new int[]{scanner.nextInt(), scanner.nextInt(), scanner.nextInt()};
}
System.out.print("请输入任务的最后终止时间:");
leastTime = scanner.nextInt();
fillValue(tasks, leastTime);
}
/**
* 动态规划填充表格并逆推出路径
* @param tasks tasks[1...n][0...2]代表n个任务的起始时间、终止时间、报酬
* @param lastTime 任务的最后终止时间
*/
public static int[] fillValue(int[][] tasks, int lastTime) {
// 动态规划表格
int[][] maxValue = new int[tasks.length][lastTime + 1];
for (int i = 1; i <= tasks.length-1 ; i++) {
for (int j = 1; j <= lastTime ; j++) {
if (j < tasks[i][1]) {
// 第i个任务的结束时间未到,不可安排
maxValue[i][j] = maxValue[i - 1][j];
} else {
// 任务可选时有两种选择,即选择当前物品与不选择当前物品
maxValue[i][j] = Math.max(maxValue[i-1][j], tasks[i][2]+maxValue[i-1][tasks[i][0]]);
}
}
}
System.out.println("总报酬:"+maxValue[tasks.length-1][lastTime]);
// 逆推出路径
int nowTime = lastTime;
int[] choose = new int[tasks.length];
for (int i = tasks.length-1; i > 0; i--){
if (maxValue[i][nowTime] == maxValue[i-1][nowTime]){
// 没有选择第i个任务
choose[i] = 0;
} else {
// 选择了第i个任务
choose[i] = 1;
nowTime = tasks[i][0];
}
}
System.out.print("安排:");
for (int i = 1; i < choose.length; i++){
System.out.print(choose[i] + " ");
}
System.out.println();
return choose;
}
}