实验内容
设有 n 个任务由 k 个可并行工作的机器来完成,完成任务 i 需要时间为 。试设计一个算法找出完成这 n 个任务的最佳调度,使完成全部任务的时间最早。(要求给出调度方案)。
程序输入:从 test 系列文件获取数据。
第一行为任务数 n 和机器个数 k。
第二行为完成任务 i 需要的时间
t
i
t_i
ti ,包含 n 个数据,以空格间隔。
例如:
3 2
2 3 4
表示有 3 个任务,2 个机器。完成 3 个任务的时间分别为 2, 3, 4。
程序输出:
输出三个测试案例所有任务完成的总时间,及调度方案。
算法设计思路
通过回溯法解决调度问题,本质上是通过遍历,本程序通过 b a c k t r a c k ( ) backtrack() backtrack()函数递归实现遍历,但在遍历的情况下,时间开销过大,时间复杂度为 O ( k n ) O(k^n) O(kn),需要剪枝来去除非解情况来减小程序运行的开销,本程序主要采取以下几种方式来剪枝:
- 众所周知,贪心算法运行效率较高,但结果不尽人意,往往得到的是次优解,我们可以利用贪心算法得到一个次优解为 B e s t T i m e BestTime BestTime赋值,再通过比较去除运行时间大于贪心次优解的情况,大大减少了程序的运行开销。
- 为
T
a
s
k
T
i
m
e
s
TaskTimes
TaskTimes排序,排序可以在回溯法中起到两个重要的作用:剪枝和优化搜索顺序。排序可以帮助我们找到一种合适的顺序,使得在搜索过程中能够更早地发现不满足条件的解,从而减少不必要的搜索和回溯操作。通过将可能的解按照某种规则排序,我们可以在搜索过程中先尝试那些更有可能满足条件的解,从而更早地发现不满足条件的情况,减少回溯的次数。排序还可以优化搜索顺序,使得在搜索过程中更有可能找到最优解。通过将可能的解按照某种规则排序,我们可以在搜索过程中先尝试那些更有可能是最优解的解,从而更快地找到最优解。这样可以减少搜索的时间复杂度,提高算法的效率。
经过剪枝优化后,程序的运行效率大大提升,测试样例1,2,3,均可在较短时间内运行完毕并给出所求结果。
源码及注释
#include <iostream>
#include <fstream>
#include <algorithm> // 包含 sort 函数
#define INF 999999999 //定义无穷大INFINITY=999999999
#define MAXSIZE 100 //定义最大任务数
using namespace std;
//定义所需全局变量
int N, K; //N:任务数,K:机器数
int taskTime[MAXSIZE]; //任务对应的时间
int bestTime = INF; //最优解:即完成全部任务最短时间,初始化为无穷大
int scheduleNow[MAXSIZE]; //当前最优调度方案,值为0表示还未分配
int best_N_to_K[MAXSIZE]; //最优解的调度方案:best_N_to_K[i]=m,表示把i+1(i从0开始算)任务分配给第m台机器
int compare(const void *a, const void *b) {
return (*(int*)b - *(int*)a);
}
void input()
{
ifstream inputFile("test1.txt"); // 打开输入文件
inputFile >> N >> K; // 读取第一行的数据
for (int i = 0; i < N; i++) {
inputFile >> taskTime[i];
}
inputFile.close();
qsort(taskTime, N, sizeof(int), compare);
}
// 返回任务完成所需的最长时间
int taskAssignment(int task_times[], int num_tasks, int num_machines) {
int* machines = (int*)calloc(num_machines, sizeof(int)); // 初始化机器耗时数组
for (int i = 0; i < num_tasks; i++) {
int min_index = 0;
for (int j = 1; j < num_machines; j++) {
if (machines[j] < machines[min_index]) {
min_index = j; // 找到当前耗时最短的机器的索引
}
}
machines[min_index] += task_times[i]; // 将任务分配给当前耗时最短的机器
}
int max_time = machines[0];
for (int i = 1; i < num_machines; i++) {
if (machines[i] > max_time) {
max_time = machines[i];
}
}
free(machines);
return max_time; // 返回最终完成所有任务所需的最长时间
}
//搜索到叶节点时,计算叶节点可行解完成任务的时间
int ScheduleTime()
{
int k[MAXSIZE] = { 0 }; //用于记录每个机器对应工作的总时间,机器下标从1开始算,因为scheduleNow[i]=0时表示未分配
for (int i = 0; i < N; i++)
{//将第i个任务时间加到第‘scheduleNow[i]’个机器中去
k[scheduleNow[i]] += taskTime[i];
}
int max = k[1];
for (int i = 1; i <= K; i++)
{//并行工作的所有机器中工作时间最长的那个时间就是叶子节点可行解时间
if (k[i] > max) max = k[i];
}
return max;
}
//排列树回溯法
void BackTrack(int deep)
{
if (deep == N)
{
int temp = ScheduleTime(); //临时存放叶节点的可行解的值
if (temp < bestTime) //可行解与当前最优解进行比较
{
bestTime = temp;
//cout<<bestTime<<endl;
for (int i = 0; i < N; i++)
{
best_N_to_K[i] = scheduleNow[i];
}
}
return;
}
for (int i = 1; i <= K; i++)
{
scheduleNow[deep] = i; //将任务deep分配给当前机器
if (ScheduleTime() < bestTime) //可行性剪枝
BackTrack(deep + 1);
scheduleNow[deep] = 0; //回溯,不将任务分配给当前机器
}
}
//打印最终可行解结果
void printSchedulePlan()
{
//针对每台机器打印其对应完成那些任务
cout << "每台机器对应完成的任务如下: \n";
for (int i = 1; i <= K; i++)
{
int time = 0;
bool hasTask = false; //hasTask用于记录机器是否有分配,若一个任务都没分配,则显示“未分配任务”
cout << "机器" << i << "分配 : ";
for (int j = 0; j < N; j++)
{
if (i == best_N_to_K[j])
{
cout << taskTime[j] << " ";
hasTask = true;
time += taskTime[j];
}
}
if (hasTask == false) cout << "未分配任务!";
cout<<"sum:"<<time<<" ";
cout << endl;
}
//打印最终结果
cout << "最优解完成所有任务的时间是:" << bestTime << endl;
}
int main()
{
input(); // 输入任务数据
bestTime = taskAssignment(taskTime,N,K);
scheduleNow[0] = 1;
BackTrack(1); // 排列树回溯法求解最优解
printSchedulePlan(); // 打印最优解
return 0;
}