一 实际问题描述

设计一个实际问题:小明要在一天内完成尽可能多的任务,每个任务有一个开始时间和结束时间,而且每个任务必须在规定的时间内完成。请使用分治法、动态规划法和贪心法等算法求解,以确定小明可以完成的最多任务数,同时给出算法的时间复杂度分析。

测试数据:假设小明有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,直到所有任务都被处理完毕。

求解过程示意图如下:

image.png

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;
}

运行结果截图:

image.png

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,直到处理完所有任务。

求解过程示意图如下:

image.png

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;
}

运行结果截图:

image.png

3.算法时间复杂度

动态规划算法的时间复杂度为O(nT),其中n为任务数,T为任务最长的完成时间。因为需要建立一个二维数组,所以需要O(nT)的空间复杂度;然后依次处理每个任务,时间复杂度为O(nT),所以总的时间复杂度为O(nT)。

(四)第三种算法------分治算法的设计与实现

1.算法解决实际问题的思路及求解过程示意图

分治算法求解本题的具体思路是:

1)按照任务结束时间从小到大排序。

2)将任务集合分成两个子集,分别求解两个子集中可以完成的最多任务数。

3)将两个子集的结果合并起来,得到整体的最优解。

4)重复步骤2和3,直到每个子集只包含一个任务。

求解过程示意图如下:

image.png

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;
}

运行结果截图:

image.png

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 著作权归作者所有。请勿转载和采集!

免费AI点我,无需注册和登录