主持人调度(二)
好的,以下是一个整理好的关于“主持人调度问题”的CSDN博客文档内容。你可以根据需要进一步调整格式或补充细节。
问题描述
有 n
个活动即将举办,每个活动都有开始时间与结束时间。第 i
个活动的开始时间是 start_i
,结束时间是 end_i
。为了成功举办每个活动,需要为每个活动分配一名主持人。一位主持人在同一时间只能参与一个活动,并且需要全程参与活动。换句话说,如果一个主持人参与了第 i
个活动,那么他在 (start_i, end_i)
这个时间段内不能参与其他任何活动。目标是求出为了成功举办所有活动,最少需要多少名主持人。
输入格式
n
:活动的数量,范围为1 ≤ n ≤ 10^5
。startEnd
:一个二维数组,startEnd[i][0]
表示第i
个活动的开始时间,startEnd[i][1]
表示第i
个活动的结束时间。时间范围为-2^32 ≤ start_i ≤ end_i ≤ 2^31 - 1
。
输出格式
- 返回一个整数,表示最少需要的主持人数量。
示例
示例 1:
输入:
2, [[1,2], [2,3]]
输出:
1
说明:只需要一个主持人就能成功举办这两个活动。
示例 2:
输入:
2, [[1,3], [2,4]]
输出:
2
说明:需要两个主持人才能成功举办这两个活动。
第一种直观解法
最开始想的传统思维,每一个活动开始前,看看前面的主持人有没有空闲的,有空闲的,就复用,没有空闲的,就再雇佣一个。
#include <stdio.h>
#include <stdlib.h>
// 定义活动结构体,每个活动包含开始时间和结束时间
typedef struct {
int start; // 活动的开始时间
int end; // 活动的结束时间
} Activity;
// 比较函数,用于 qsort 排序
int compare(const void* a, const void* b) {
Activity* activity1 = (Activity*)a; // 将 void* 类型转换为 Activity* 类型
Activity* activity2 = (Activity*)b; // 将 void* 类型转换为 Activity* 类型
// 首先按活动的开始时间排序
if (activity1->start < activity2->start) return -1; // 如果活动1的开始时间更早,返回 -1注意可能会越界,所以用比较,而不是相减
if (activity1->start > activity2->start) return 1; // 如果活动1的开始时间更晚,返回 1
// 如果开始时间相同,则按结束时间排序
if (activity1->end < activity2->end) return -1; // 如果活动1的结束时间更早,返回 -1
if (activity1->end > activity2->end) return 1; // 如果活动1的结束时间更晚,返回 1
return 0; // 如果两个活动的开始时间和结束时间都相同,返回 0
}
// 计算最少需要的主持人数量
int minmumNumberOfHost(int n, int** startEnd, int startEndRowLen, int* startEndColLen) {
// 创建一个数组来存储所有活动的开始时间和结束时间
Activity activities[n];
for (int i = 0; i < n; i++) {
activities[i].start = startEnd[i][0]; // 获取第 i 个活动的开始时间
activities[i].end = startEnd[i][1]; // 获取第 i 个活动的结束时间
}
// 使用 qsort 对活动数组进行排序,排序规则由 compare 函数定义
qsort(activities, n, sizeof(Activity), compare);
// 创建一个数组来记录每个主持人的空闲时间
int hosts[n]; // 假设最多可能需要 n 个主持人
int hostCount = 0; // 当前已分配的主持人数量
// 遍历所有活动,尝试为每个活动分配主持人
for (int i = 0; i < n; i++) {
int assigned = 0; // 标记当前活动是否已经分配了主持人
// 尝试复用已分配的主持人
for (int j = 0; j < hostCount; j++) {
// 如果当前活动的开始时间大于等于某个主持人的空闲时间,则可以复用该主持人
if (activities[i].start >= hosts[j]) {
hosts[j] = activities[i].end; // 更新该主持人的空闲时间为当前活动的结束时间
assigned = 1; // 标记当前活动已分配主持人
break; // 跳出循环
}
}
// 如果当前活动无法复用任何已分配的主持人,则新增一名主持人
if (!assigned) {
hosts[hostCount] = activities[i].end; // 新增主持人的空闲时间为当前活动的结束时间
hostCount++; // 主持人数量加 1
}
}
return hostCount; // 返回所需的最少主持人数量
}
代码逻辑详解
-
活动结构体:
- 定义了一个
Activity
结构体,包含两个字段:start
和end
,分别表示活动的开始时间和结束时间。
- 定义了一个
-
比较函数:
compare
函数用于qsort
排序。它首先按活动的开始时间排序,如果开始时间相同,则按结束时间排序。- 这种排序方式确保了活动按时间顺序排列,便于后续的贪心算法实现。
-
活动数组初始化:
- 遍历输入的二维数组
startEnd
,将每个活动的开始时间和结束时间存入activities
数组。
- 遍历输入的二维数组
-
排序:
- 使用
qsort
对活动数组按排序规则进行排序。
- 使用
-
分配主持人:
- 创建一个数组
hosts
用于记录每个主持人的空闲时间。 - 遍历排序后的活动数组,尝试为每个活动分配主持人:
- 如果当前活动的开始时间大于等于某个主持人的空闲时间,则可以复用该主持人,并更新该主持人的空闲时间为当前活动的结束时间。
- 如果无法复用任何已分配的主持人,则新增一名主持人。
- 创建一个数组
-
返回结果:
- 最终返回所需的最少主持人数量。
贪心算法的关键点
- 排序:通过排序确保活动按时间顺序排列,便于贪心选择。
- 复用主持人:尽量复用已有的主持人,避免新增主持人。
- 空闲时间更新:每次分配活动后,更新主持人的空闲时间。
复杂度分析
- 时间复杂度:
O(n log n)
,主要由排序操作决定。 - 空间复杂度:
O(n)
,用于存储活动数组和主持人数组。
第二种思路
代码解析
int cmp(const void* a, const void* b) {
return *(int*)a - *(int*)b; // 比较函数,用于qsort排序,从小到大排序
}
cmp
函数:这是qsort
函数的比较函数,用于对数组进行排序。它将两个指针指向的整数进行比较,返回它们的差值。如果差值为负数,则表示第一个数小于第二个数;如果差值为正数,则表示第一个数大于第二个数。这里实现的是从小到大排序。
int minmumNumberOfHost(int n, int** startEnd, int startEndRowLen, int* startEndColLen) {
// 分配内存用于存储所有活动的开始时间和结束时间
int* Start = (int*)malloc(sizeof(int) * n);
int* End = (int*)malloc(sizeof(int) * n);
// 将输入的活动开始时间和结束时间分别存储到Start和End数组中
for (int i = 0; i < n; i++) {
Start[i] = startEnd[i][0]; // 第i个活动的开始时间
End[i] = startEnd[i][1]; // 第i个活动的结束时间
}
// 对开始时间和结束时间分别进行排序
qsort(Start, n, sizeof(int), cmp);
qsort(End, n, sizeof(int), cmp);
Start
和End
数组:分别存储所有活动的开始时间和结束时间。qsort
函数:对Start
和End
数组分别进行排序。排序后,Start
数组中的元素按从小到大的顺序表示所有活动的开始时间,End
数组中的元素按从小到大的顺序表示所有活动的结束时间。
int i, j;
for (i = 1, j = 0; i < n; i++) {
if (Start[i] >= End[j]) {
j++; // 如果当前活动的开始时间大于等于某个活动的结束时间,说明可以复用主持人
}
}
- 双指针逻辑:
i
指针遍历所有活动的开始时间(Start
数组)。j
指针遍历所有活动的结束时间(End
数组)。- 对于每个活动的开始时间
Start[i]
,检查是否存在一个活动的结束时间End[j]
,使得Start[i] >= End[j]
。如果满足条件,说明当前活动可以复用已经结束的活动的主持人,因此j
指针向前移动。 - 如果
Start[i] < End[j]
,说明当前活动无法复用任何已结束的活动的主持人,需要新增一名主持人。
可以理解为每次活动开始的时候,问下前面的活动有没有结束的,赶紧来个主持人,由于end已经排序好了,所以不用担心,只要比较第j个(初始为0)就好了,第一个不行,之后更加完蛋,然后第一个活动主持人如果被叫过去,j++;
return i - j; // 返回需要的主持人数量
}
- 返回值:
i - j
表示需要的主持人数量。i
是总活动数量,j
表示可以复用的活动数量。因此,i - j
就是需要新增的主持人数量。
完整代码与注释
int cmp(const void* a, const void* b) {
return *(int*)a - *(int*)b; // 比较函数,用于qsort排序,从小到大排序
}
int minmumNumberOfHost(int n, int** startEnd, int startEndRowLen, int* startEndColLen) {
// 分配内存用于存储所有活动的开始时间和结束时间
int* Start = (int*)malloc(sizeof(int) * n);
int* End = (int*)malloc(sizeof(int) * n);
// 将输入的活动开始时间和结束时间分别存储到Start和End数组中
for (int i = 0; i < n; i++) {
Start[i] = startEnd[i][0]; // 第i个活动的开始时间
End[i] = startEnd[i][1]; // 第i个活动的结束时间
}
// 对开始时间和结束时间分别进行排序
qsort(Start, n, sizeof(int), cmp);
qsort(End, n, sizeof(int), cmp);
// 双指针逻辑:i遍历开始时间,j遍历结束时间
int i, j;
for (i = 1, j = 0; i < n; i++) {
if (Start[i] >= End[j]) {
j++; // 如果当前活动的开始时间大于等于某个活动的结束时间,说明可以复用主持人
}
}
// 返回需要的主持人数量
return i - j; // i是总活动数量,j是可复用的活动数量
}
算法逻辑总结
-
分离和排序:
- 将所有活动的开始时间和结束时间分别存储到两个数组
Start
和End
中。 - 对
Start
和End
数组分别进行排序。
- 将所有活动的开始时间和结束时间分别存储到两个数组
-
双指针遍历:
- 使用
i
指针遍历所有活动的开始时间。 - 使用
j
指针遍历所有活动的结束时间。 - 如果
Start[i] >= End[j]
,说明当前活动可以复用某个已结束的活动的主持人,j
指针向前移动。 - 如果
Start[i] < End[j]
,说明当前活动需要新增一名主持人。
- 使用
-
计算结果:
- 最终返回
i - j
,即需要的主持人数量。
- 最终返回
复杂度分析
- 时间复杂度:
O(n log n)
,主要由两次qsort
排序操作决定。 - 空间复杂度:
O(n)
,用于存储Start
和End
数组。
示例验证
详细分析例子5, [[1,3],[2,4],[2,8],[3,4],[4,10]]
,并逐步解释代码是如何处理的。
输入数据
- 活动数量:
n = 5
- 活动的开始时间和结束时间:
[[1,3], [2,4], [2,8], [3,4], [4,10]]
代码执行过程
1. 提取并排序开始时间和结束时间
代码首先将所有活动的开始时间和结束时间分别提取到两个数组 Start
和 End
中,然后对这两个数组分别进行排序。
提取后的数组:
Start
:[1, 2, 2, 3, 4]
End
:[3, 4, 4, 8, 10]
排序后的数组:
Start
:[1, 2, 2, 3, 4]
(排序后不变)End
:[3, 4, 4, 8, 10]
(排序后)
2. 双指针遍历
接下来,代码使用双指针 i
和 j
来判断哪些活动可以复用已有的主持人。
int i, j;
for (i = 1, j = 0; i < n; i++) {
if (Start[i] >= End[j]) {
j++; // 如果当前活动的开始时间大于等于某个活动的结束时间,说明可以复用主持人
}
}
双指针遍历过程:
-
初始状态:
i = 1
,j = 0
Start[i] = 2
,End[j] = 3
2 < 3
,说明活动[2,4]
无法复用任何已结束的活动的主持人。
-
第二轮:
i = 2
,j = 0
Start[i] = 2
,End[j] = 3
2 < 3
,说明活动[2,8]
也无法复用任何已结束的活动的主持人。
-
第三轮:
i = 3
,j = 0
Start[i] = 3
,End[j] = 3
3 >= 3
,说明活动[3,4]
可以复用活动[1,3]
的主持人,因此j++
。
-
第四轮:
i = 4
,j = 1
Start[i] = 4
,End[j] = 4
4 >= 4
,说明活动[4,10]
可以复用活动[2,4]
的主持人,因此j++
。
最终状态:
i = 5
(遍历完所有活动)j = 2
(有两个活动可以复用已有的主持人)
3. 计算结果
最后,代码返回所需的主持人数量:
return i - j; // i 是总活动数量,j 是可以复用的活动数量
计算结果:
i = 5
(总活动数量)j = 2
(可以复用的活动数量)- 所需主持人数量 =
5 - 2 = 3
结论
对于输入 5, [[1,3],[2,4],[2,8],[3,4],[4,10]]
,代码的输出是 3
,表示需要 3名主持人 来成功举办所有活动。
验证
我们可以通过手动分析来验证这个结果:
- 活动
[1,3]
需要一个主持人。 - 活动
[2,4]
和[2,8]
与[1,3]
有时间冲突,因此需要两个新的主持人。 - 活动
[3,4]
可以复用[1,3]
的主持人。 - 活动
[4,10]
可以复用[2,4]
的主持人。
因此,总共需要 3名主持人,与代码的输出一致。