任务安排问题:贪心算法、动态规划算法和分治算法的应用
一 实际问题描述
设计一个实际问题:小明要在一天内完成尽可能多的任务,每个任务有一个开始时间和结束时间,而且每个任务必须在规定的时间内完成。请使用分治法、动态规划法和贪心法等算法求解,以确定小明可以完成的最多任务数,同时给出算法的时间复杂度分析。
测试数据:假设小明有8个任务,任务的开始时间和结束时间如下表所示,时间以小时为单位。
| 任务编号 | 开始时间 | 结束时间 | | :------: | :------: | :------: | | 1 | 9 | 10 | | 2 | 9 | 12 | | 3 | 10 | 11 | | 4 | 10 | 13 | | 5 | 11 | 12 | | 6 | 11 | 14 | | 7 | 12 | 13 | | 8 | 13 | 14 |
小明必须在规定的时间内完成任务,且不能在同一时间完成多个任务。
二 算法设计与实现过程
(一)所选用的算法及原因
所选用的算法有分治法、动态规划法和贪心法。分治法可以快速地将问题规模缩小,使得问题变得简单易解;动态规划法可以处理有重叠子问题和最优子结构性质的问题;贪心法可以求解一些具有贪心选择性质的问题。
对于本题,分治法需要将任务按照时间顺序划分成若干个子集,然后分别求解每个子集中可以完成的最多任务数,最后将所有子集的结果合并起来,得到整体的最优解;动态规划法需要建立一个二维数组,来保存每个任务的最优解,然后逐个计算每个任务的最优解,最后得到整体的最优解;贪心法需要按照任务结束时间从小到大排序,然后依次选择结束时间最早的任务,如果该任务与已经选择的任务不冲突,则将其加入最终的任务列表中。
因为本题具有最优子结构性质和贪心选择性质,所以可以使用动态规划法和贪心法求解。
(二)第一种算法------贪心算法的设计与实现
1.算法解决实际问题的思路及求解过程示意图
贪心算法求解本题的具体思路是:
1)按照任务结束时间从小到大排序。
2)依次选择结束时间最早的任务,如果该任务与已经选择的任务不冲突,则将其加入最终的任务列表中。
3)重复步骤2,直到所有任务都被处理完毕。
求解过程示意图如下:

2.算法程序代码及运行结果截图
#include <stdio.h>
#include <stdlib.h>
typedef struct task {
int id; // 任务编号
int start; // 任务开始时间
int end; // 任务结束时间
} Task;
int cmp(const void *a, const void *b) {
Task ta = *(Task *)a;
Task tb = *(Task *)b;
return ta.end - tb.end;
}
int greedy(Task tasks[], int n, Task selected[]) {
int count = 0;
int last_end_time = 0;
for (int i = 0; i < n; i++) {
if (tasks[i].start >= last_end_time) {
selected[count++] = tasks[i];
last_end_time = tasks[i].end;
}
}
return count;
}
int main() {
Task tasks[] = {
{1, 9, 10},
{2, 9, 12},
{3, 10, 11},
{4, 10, 13},
{5, 11, 12},
{6, 11, 14},
{7, 12, 13},
{8, 13, 14}
};
int n = sizeof(tasks) / sizeof(tasks[0]);
qsort(tasks, n, sizeof(tasks[0]), cmp);
Task selected[n];
int count = greedy(tasks, n, selected);
printf("最多可以完成%d个任务,任务列表如下:\n", count);
for (int i = 0; i < count; i++) {
printf("任务%d:开始时间%d,结束时间%d\n", selected[i].id, selected[i].start, selected[i].end);
}
return 0;
}
运行结果截图:

3.算法时间复杂度
贪心算法的时间复杂度为O(nlogn),其中n为任务数。因为需要对任务按照结束时间进行排序,所以需要O(nlogn)的时间复杂度,然后依次处理每个任务,时间复杂度为O(n),所以总的时间复杂度为O(nlogn)。
(三)第二种算法------动态规划算法的设计与实现
1.算法解决实际问题的思路及求解过程示意图
动态规划算法求解本题的具体思路是:
1)按照任务结束时间从小到大排序。
2)建立一个二维数组dp,其中dp[i][j]表示在前i个任务中,完成时间不超过j的最多任务数。
3)对于第i个任务,如果该任务的结束时间小于等于j,则可以选择该任务,此时dp[i][j] = dp[i-1][j-tasks[i].start] + 1;否则不选该任务,dp[i][j] = dp[i-1][j]。
4)重复步骤3,直到处理完所有任务。
求解过程示意图如下:

2.算法程序代码及运行结果截图
#include <stdio.h>
#include <stdlib.h>
typedef struct task {
int id; // 任务编号
int start; // 任务开始时间
int end; // 任务结束时间
} Task;
int cmp(const void *a, const void *b) {
Task ta = *(Task *)a;
Task tb = *(Task *)b;
return ta.end - tb.end;
}
int dynamic(Task tasks[], int n) {
int dp[n+1][24] = {0};
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= 23; j++) {
if (tasks[i-1].end <= j) {
dp[i][j] = dp[i-1][j-tasks[i-1].start] + 1;
} else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][23];
}
int main() {
Task tasks[] = {
{1, 9, 10},
{2, 9, 12},
{3, 10, 11},
{4, 10, 13},
{5, 11, 12},
{6, 11, 14},
{7, 12, 13},
{8, 13, 14}
};
int n = sizeof(tasks) / sizeof(tasks[0]);
qsort(tasks, n, sizeof(tasks[0]), cmp);
int count = dynamic(tasks, n);
printf("最多可以完成%d个任务\n", count);
return 0;
}
运行结果截图:

3.算法时间复杂度
动态规划算法的时间复杂度为O(nT),其中n为任务数,T为任务最长的完成时间。因为需要建立一个二维数组,所以需要O(nT)的空间复杂度;然后依次处理每个任务,时间复杂度为O(nT),所以总的时间复杂度为O(nT)。
(四)第三种算法------分治算法的设计与实现
1.算法解决实际问题的思路及求解过程示意图
分治算法求解本题的具体思路是:
1)按照任务结束时间从小到大排序。
2)将任务集合分成两个子集,分别求解两个子集中可以完成的最多任务数。
3)将两个子集的结果合并起来,得到整体的最优解。
4)重复步骤2和3,直到每个子集只包含一个任务。
求解过程示意图如下:

2.算法程序代码及运行结果截图
#include <stdio.h>
#include <stdlib.h>
typedef struct task {
int id; // 任务编号
int start; // 任务开始时间
int end; // 任务结束时间
} Task;
int cmp(const void *a, const void *b) {
Task ta = *(Task *)a;
Task tb = *(Task *)b;
return ta.end - tb.end;
}
int conquer(Task tasks[], int start, int end) {
if (start >= end) {
return 1;
} else if (tasks[end-1].end <= tasks[start].start) {
return end - start;
} else {
int mid = (start + end) / 2;
int left = conquer(tasks, start, mid);
int right = conquer(tasks, mid, end);
return (left > right ? left : right);
}
}
int divide(Task tasks[], int n) {
return conquer(tasks, 0, n);
}
int main() {
Task tasks[] = {
{1, 9, 10},
{2, 9, 12},
{3, 10, 11},
{4, 10, 13},
{5, 11, 12},
{6, 11, 14},
{7, 12, 13},
{8, 13, 14}
};
int n = sizeof(tasks) / sizeof(tasks[0]);
qsort(tasks, n, sizeof(tasks[0]), cmp);
int count = divide(tasks, n);
printf("最多可以完成%d个任务\n", count);
return 0;
}
运行结果截图:

3.算法时间复杂度
分治算法的时间复杂度为O(nlogn),其中n为任务数。因为需要对任务按照结束时间进行排序,所以需要O(nlogn)的时间复杂度,然后将任务集合分成两个子集,每个子集的规模为n/2,递归求解两个子集,时间复杂度为2 * T(n/2),合并两个子集的结果,时间复杂度为O(1),所以总的时间复杂度为T(n) = 2 * T(n/2) + O(nlogn) = O(nlogn)。
三 说明未使用的三种算法能解决或不能解决该问题的原因
1.蛮力算法能解决该问题/不能解决该问题的原因
蛮力算法可以解决该问题,但效率较低。蛮力算法需要枚举所有可能的任务组合,然后判断每个组合是否满足条件,最后找到满足条件的任务组合中,任务数最多的一个。该算法的时间复杂度为O(2^n),其中n为任务数,效率较低。
2.回溯算法能解决该问题/不能解决该问题的原因
回溯算法可以解决该问题,但效率不如贪心算法和动态规划算法。回溯算法需要从第一个任务开始,依次考虑选择或不选择每个任务,如果选择该任务,则继续考虑下一个任务,否则跳过该任务,继续考虑下一个任务,直到所有任务都被处理完毕。该算法的时间复杂度为O(2^n),其中n为任务数,效率不如贪心算法和动态规划算法。
3.分支限界算法能解决该问题/不能解决该问题的原因
分支限界算法可以解决该问题,但效率不如贪心算法和动态规划算法。分支限界算法需要建立一个搜索树,每个节点代表一个任务组合,然后按照某种策略搜索树,找到满足条件的任务组合中,任务数最多的一个。该算法的时间复杂度为O(2^n),其中n为任务数,效率不如贪心算法和动态规划算法。
总的来说,对于本题,贪心算法和动态规划算法的效率更高,因为它们可以利用问题的特殊性质,避免了对所有可能的任务组合进行枚举。分治算法也能解决该问题,但效率不如贪心算法和动态规划算法。蛮力算法、回溯算法和分支限界算法也可以解决该问题,但效率较低,不适合解决大规模问题。
原文地址: https://www.cveoy.top/t/topic/oR2q 著作权归作者所有。请勿转载和采集!