题目:
假设有n个任务由k个可并行工作的机器完成,完成任务i需要的时间为ti,试设计一个算法找出完成这n个任务的最佳调度,使得完成全部任务的时间最早。
之前算法课设的时候遇到的题目,发现市面上大部分的博客都是采用的C语言书写的这题,即使有解决方案,大多也是复制粘贴根本没有把思路讲清楚,所以我翻了翻之前的代码,努力把自己的思路讲清楚,包括详细步骤的描述,解空间树的绘制,也算是给自己的算法课设做个总结。
解题思路:
每一个任务都有K个选择,解空间树也就是一颗深度为K的满K叉树,首先我们选择一条路径往下走,以DFS(深度优先遍历)的当时来搜索整个解空间。每次搜索结束都记录下bestTime和与之对应的路径。一开始我们需要完整的走完一条路径,后面再进行遍历的时候只需要与现有的数据进行比较,当我们发现当前累积的时间已经超过当前的最优解时便停止遍历,即进行剪枝操作。
开始结点就成为一个活结点,同时也成为当前的扩展结点。在当前的扩展结点处向纵深方向移至一个新结点,并成为一个新的活结点,也成为当前扩展结点。 如果在当前的扩展结点处不能再向纵深方向扩展,则当前扩展结点就成为死结点。此时,应往回移动(回溯)至最近的一个活结点处,并使这个活结点成为当前的扩展结点;直至找到一个解或全部解。
详细的步骤说明:
1.将所有的任务都放置机器一中,不考虑其他机器,这也是我们深度优先遍历的第一步,将这个作为初始值,此时的bestTime为50,最佳调度路径为:1 1 1 1 1 1 1。
2.最后一个任务我们在分配的时候有三种选择,即机器一、二、三。
我们此时执行结束的代码为:
if(dep == numberOfTask){ // 达到了叶子节点
int maxTime = compute(); // 计算完整调度的完成时间
if(maxTime < best){ // 当前的时间比最优解还要少
best = maxTime; // 更新时间
for(int i = 0; i < numberOfTask; i++){
bestx[i] = path[i]; // 更新路径
}
}
return;
}
因为已经记录下了最佳时间,我们return回去,回到上一级中的这一段代码:
// 尚未到达叶子节点,向纵深方向进行拓展
for(int i = 0; i < numberOfMachine; i++){ // 把任务加到第i个机器里面去
len[i] += timeOfTask[dep];
path[dep] = i+1; // 记录下当前的临时路径
if(len[i] < best){ // 如果已经大于了当前的最优时间,就不用继续了,放弃这一个任务
backtrack(dep+1);
}
len[i] -= timeOfTask[dep]; // 回溯 减去这一任务的值,恢复这一机器的x【i】,证明这一机器不要这一任务,就去找其他机器。
}
此时,我们的前(n-1)个任务都已经分配在了第一个机器中,return操作执行之前,第n个任务我们分配在了第一个机器,所得到的最佳时间为50,此时我们回溯到了上一步,第n个任务面临三种选择,我们将执行第二个选择,也就是第二台机器。
这时,我们留意到,此刻的最佳时间已经变成了47,这就执行了更新操作,我们再次执行return操作回到上一步。
3.由于之前我们将任务n分配给了机器一和二,这时我们的for循环将把任务n分配给机器三。
此刻的时间还是47,不需要执行更新操作,然后我们回到第(n-1)个节点,它也将面临三个选择。
4.将第(n-1)个节点分配给机器二
然后我们发现时间再次缩短为了45,更新时间和最佳路径。由于任务(n-1)分配给了机器二,尚未到达叶子节点,所以还需执行步骤1、2、3将任务n分配给机器一、二、三,进行时间的判断操作,如此反复即可。
核心代码:
/**
* 回溯法解决问题
* 当dep>n时,算法搜索至叶子结点,得到一个新的作业调度方案。此时算法适时更新当前最优值和相应的当前最佳调度。
* 当dep<n时,若当前扩展结点位于排列树的第(n-1)层,此时算法选择下一个要安排的作业进行搜索且向第(n-2)层回溯,
* 以深度优先方式递归的对相应的子树进行搜索,对不满足上界约束的结点,则剪去相应的子树向第(n-2)层回溯。
* @param dep
*/
private void backtrack(int dep) {
if(dep == numberOfTask){ // 达到了叶子节点
int maxTime = compute(); // 计算完整调度的完成时间
if(maxTime < best){ // 当前的时间比最优解还要少
best = maxTime; // 更新时间
for(int i = 0; i < numberOfTask; i++){
bestx[i] = path[i]; // 更新路径
}
}
return;
}
// 尚未到达叶子节点,向纵深方向进行拓展
for(int i = 0; i < numberOfMachine; i++){ // 把任务加到第i个机器里面去
len[i] += timeOfTask[dep];
path[dep] = i+1; // 记录下当前的临时路径
if(len[i] < best){ // 如果已经大于了当前的最优时间,就不用继续了,放弃这一个任务
backtrack(dep+1);
}
len[i] -= timeOfTask[dep]; // 回溯 减去这一任务的值,恢复这一机器的x【i】,证明这一机器不要这一任务,就去找其他机器。
}
}
没什么好说的,代码里面注释很详细,主要使用了回溯法以及剪枝的操作。
完整代码:
/**
* @ Author :heywecome
* @ Date :Created in 14:24 2018/12/23
* @Version: $version$
*/
public class MachineWork {
// 假设有n个任务由k个可并行工作的机器完成。完成任务i需要的时间为ti。
// 试设计一个算法找出完成这n个任务的最佳调度,使得完成全部任务的时间最早
int numberOfTask; //任务数
int numberOfMachine; //机器数
int best; // 记录最优的时间
int[] timeOfTask; //每个任务所需的时间序列
int[] len; // 每台机器所需时间序列
int[] path; // 当前路径
int[] bestx; // 最优调度:其中bestx[i]=m表示把第i项任务分配给第m台机器
/**
* 思路:回溯法 剪枝
* 一个k个可并行的机器,实际就相当于把这k个机器当做容器,将时间填入容器中去,按照时间来搜索,
* 优化:
* 最优化剪枝:如果任何一个机器超过已知最优解则停止向下搜索
* 排序:从大到小排序任务时间可以减少搜索时间
*/
public MachineWork(int numberOfTask, int numberOfMachine, int[] timeOfTask){
this.numberOfTask = numberOfTask;
this.numberOfMachine = numberOfMachine;
this.timeOfTask = timeOfTask;
}
/**
* 初始化必要参数
*/
private void init(){
len = new int[numberOfMachine]; // 记录每个机器已经安排的时间
best = Integer.MAX_VALUE;
bestx = new int[numberOfTask];
path = new int[numberOfTask];
}
/**
* 打印最后的结果
*/
private void printSolution(){
Long startTime = System.nanoTime();
backtrack(0);
Long endTime = System.nanoTime();
System.out.println("总共消耗的时间为: " + (endTime - startTime) + " ns");
System.out.print("最优时间为:");
System.out.println(best);
System.out.println("每个任务消耗的时间为:");
for (int i=0;i<numberOfTask;i++){
System.out.print(timeOfTask[i]+" ");
}
System.out.println();
System.out.println("具体的规划如下:");
for (int i=0;i<numberOfTask;i++){
System.out.print(bestx[i]+" ");
}
}
public void findTheSolution(){
init();
printSolution();
}
/**
* 回溯法解决问题
* 当dep>n时,算法搜索至叶子结点,得到一个新的作业调度方案。此时算法适时更新当前最优值和相应的当前最佳调度。
* 当dep<n时,若当前扩展结点位于排列树的第(n-1)层,此时算法选择下一个要安排的作业进行搜索且向第(n-2)层回溯,
* 以深度优先方式递归的对相应的子树进行搜索,对不满足上界约束的结点,则剪去相应的子树向第(n-2)层回溯。
* @param dep
*/
private void backtrack(int dep) {
if(dep == numberOfTask){ // 达到了叶子节点
int maxTime = compute(); // 计算完整调度的完成时间
if(maxTime < best){ // 当前的时间比最优解还要少
best = maxTime; // 更新时间
for(int i = 0; i < numberOfTask; i++){
bestx[i] = path[i]; // 更新路径
}
}
return;
}
// 尚未到达叶子节点,向纵深方向进行拓展
for(int i = 0; i < numberOfMachine; i++){ // 把任务加到第i个机器里面去
len[i] += timeOfTask[dep];
path[dep] = i+1; // 记录下当前的临时路径
if(len[i] < best){ // 如果已经大于了当前的最优时间,就不用继续了,放弃这一个任务
backtrack(dep+1);
}
len[i] -= timeOfTask[dep]; // 回溯 减去这一任务的值,恢复这一机器的x【i】,证明这一机器不要这一任务,就去找其他机器。
}
}
// 计算一个完整调度的完成时间,找出所有机器中的最大值
// len每台机器所需时间序列
private int compute(){
int tmp = 0;
for(int i = 0; i < numberOfMachine; i++){
if(len[i] > tmp){
tmp = len[i];
}
}
return tmp;
}
public static void main(String[] args){
int numberOfTask = 7; // 任务数量
int numberOfMachine = 3; // 机器数量
int[] timeOfTask = new int[]{2, 14, 4, 16, 6, 5, 3}; // 每个任务的时间
MachineWork machine =new MachineWork(numberOfTask, numberOfMachine, timeOfTask);
machine.findTheSolution();
}
}
解空间树的绘制:
由于时间不是很充沛,后续单调的操作我就不画了,自行补充...
————————————————
版权声明:本文为CSDN博主「何大康说」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/HeyWeCome/article/details/93887288