1.什么是贪心算法
我的理解:
贪心算法是一种常用的问题求解方法,它在每个步骤上都选择当前看起来最优的解,而不考虑整体的最优解。简单来说,贪心算法采取局部最优的决策,希望通过每个局部最优解的选择,最终得到全局最优解。
贪心算法的基本思想是通过贪心选择策略,一步一步地构建解决方案,每次选择都是基于当前情况下的最优决策。它通常适用于满足最优子结构的问题,即一个问题的最优解可以通过子问题的最优解来构建。
然而,需要注意的是,贪心算法并不是适用于所有问题的通用解决方案。有时候贪心选择并不能保证得到全局最优解,可能会导致局部最优解并不是整体最优解的情况。在使用贪心算法时,需要仔细分析问题的特性,确保贪心选择的正确性。
总之,贪心算法是一种简单而有效的问题求解方法,通过不断选择当前最优解,希望最终达到全局最优解。它在一些问题上具有高效性和简洁性,但并非适用于所有情况。
具体例子:
我们来举一个具体的例子来说明贪心算法的应用。
假设你是一个商人,要在一天内走访多个城市,每个城市都有一定数量的客户等待你的拜访。你想要制定一个行程计划,使得在有限的时间内拜访尽可能多的客户。
这个问题可以使用贪心算法来解决。一种贪心策略是每次选择距离当前位置最近的客户进行拜访。你可以从起始城市开始,选择最近的客户进行拜访,然后移动到下一个最近的客户,一直重复这个过程直到时间用尽。
这种贪心策略的原理是每次选择距离最近的客户,最大限度地减少行程中的路程,从而拜访更多的客户。虽然这个策略并不能保证一定能够找到最优解,但在很多情况下它能够得到接近最优解的结果,并且具有较高的效率。
需要注意的是,这个例子中贪心算法的应用是基于问题的特性,即选择最近的客户是每一步的局部最优解,并且每次选择不会影响到后续的决策。因此,贪心算法在这个问题上是有效的。
2.什么是活动安排问题?
活动安排问题是贪心算法的经典应用之一。它的场景是给定一组活动,每个活动都有一个开始时间和结束时间,目标是安排出一个最大数量的相互兼容的活动集合,即这些活动之间不会相互冲突。
贪心算法在解决活动安排问题时的思路是选择结束时间最早的活动,这样可以腾出更多的时间去安排其他活动。具体步骤如下:
1. 将活动按照结束时间从早到晚进行排序。
2. 选择结束时间最早的活动作为第一个安排的活动。
3. 从剩余的活动中选择开始时间不早于已安排活动的结束时间的活动,并且结束时间最早。
4. 重复步骤3,直到没有可选择的活动为止。
贪心算法的关键在于证明这个贪心策略的正确性。对于活动安排问题,可以证明贪心选择最优解。假设存在一个最优解,其中包含了活动A和活动B,而我们的贪心策略没有选择其中一个。通过比较A和B的结束时间,我们可以发现选择结束时间较早的活动,能够为后续的活动安排腾出更多的空间。因此,我们的贪心策略可以得到最优解。
活动安排问题是贪心算法的经典案例,它的时间复杂度为O(nlogn),其中n是活动的数量。这个问题在诸如会议安排、课程表编排等实际场景中有着广泛的应用。
3.为什么要从结束时间开始排序
在活动安排问题中,选择按照结束时间来排序是因为这样的贪心策略能够最大程度地腾出时间空间,以容纳更多的活动。
考虑一下,如果我们选择按照开始时间来排序。在这种情况下,我们可能会选择一个很早开始但是持续时间很长的活动,这可能会占用较大的时间段,导致其他活动无法安排进来。
然而,如果我们选择按照结束时间来排序,我们能够优先选择结束时间早的活动,这样能够更快地释放出时间段,为后续的活动提供更多的可用时间。这种策略可以最大化地利用时间资源,以安排尽可能多的活动。
当然,按照结束时间排序并不是在所有情况下都适用的通用策略。有些特定情况下,可能会存在其他的排序策略。但在活动安排问题中,按照结束时间排序是常见且有效的贪心策略,它能够提供一个近似的最优解。
4.GreedySelector算法
完整功能代码:
template<class Type>
void GreedySelector(int n, Type s[], Type f[], bool A[])
{
A[1] = true;
int j = 1;
for (int i = 2; i <= n; i++)
{
if (s[i] >= f[j])
{
A[i] = true;
j = i;
}
else
{
A[i] = false;
}
}
}
这段代码实现了一个贪心选择算法来解决活动安排问题。让我逐行解释代码的功能:
template<class Type>
void GreedySelector(int n, Type s[], Type f[], bool A[])
这是一个模板函数的定义,它接受三个数组参数和一个布尔类型的输出数组。`n` 是活动的数量,`s[]` 是活动的开始时间数组,`f[]` 是活动的结束时间数组,`A[]` 是用来记录选择结果的布尔数组。
A[1] = true;
int j = 1;
将第一个活动设置为选中状态(`true`),并初始化变量 `j` 为 1,用于记录上一个选中的活动的索引。
for (int i = 2; i <= n; i++)
{
if (s[i] >= f[j])
{
A[i] = true;
j = i;
}
else
{
A[i] = false;
}
}
从第二个活动开始遍历,判断当前活动的开始时间 `s[i]` 是否大于等于上一个选中的活动的结束时间 `f[j]`。如果满足这个条件,说明当前活动与前一个活动不冲突,可以选中该活动,并更新 `j` 为当前活动的索引。否则,当前活动与前一个活动冲突,不能选中该活动。
最终,布尔数组 `A[]` 记录了最优解中哪些活动被选中(`true`)或未被选中(`false`)。
这段代码的关键在于每次选择活动时,判断当前活动是否与前一个选中的活动冲突,只选取不冲突的活动。通过这种贪心选择策略,可以得到一个最大数量的相互兼容的活动集合。
5.关于书上文字的解释:
这段文字是对贪心选择算法(Greedvselector)的解释。它使用集合 A 来存储所选择的活动,其中活动 i 在集合 A 中当且仅当 A[i] 的值为 true。变量 j 用于记录最近一次加入到 A 中的活动。由于输入的活动是按照它们的结束时间非递减序排列的,所以 f[j] 总是当前集合 A 中所有活动的最大结束时间,即 f[j] = max{f[i]}。
贪心算法 GreedySelector 从活动 1 开始选择,并将 j 初始化为 1,然后依次检查活动 i 是否与当前已选择的所有活动相容。如果相容,则将活动 i 加入到已选择活动的集合 A 中;否则不选择活动 i,而继续检查下一个活动与集合 A 中活动的相容性。由于 f[j] 总是当前集合 A 中所有活动的最大结束时间,所以活动 i 与当前集合 A 中所有活动相容的充分且必要条件是,其开始时间 s[i] 不早于最近加入集合 A 中的活动的结束时间 f[j]。如果活动 i 与之相容,则 i 成为最近加入集合 A 中的活动,并取代活动 j 的位置。由于输入的活动以其结束时间的非递减排列,因此算法 GreedySelector 每次总是选择具有最早结束时间的相容活动加入集合 A 中。按照这种方法选择相容活动可以尽可能地留下更多的时间段来安排未选择的活动,从而使剩余的可安排时间段最大化,以容纳尽可能多的相容活动。
贪心算法 GreedySelector 的效率非常高。当输入的活动已按照结束时间的非递减序排列时,算法只需 O(n) 的时间复杂度来安排活动,其中 n 是待安排的活动数量,使最多的活动能相容地使用公共资源。
6.代码实现:
C语言版:
#include <stdio.h>
void GreedySelector(int n, int s[], int f[], int A[]) {
A[0] = 1; // 第一个活动总是选中
int j = 0; // 记录最近一次加入到 A 中的活动的索引
for (int i = 1; i < n; i++) {
if (s[i] >= f[j]) {
A[i] = 1; // 选择活动 i
j = i;
} else {
A[i] = 0; // 不选择活动 i
}
}
}
int main() {
int n = 5; // 活动数量
int s[] = {1, 3, 0, 5, 8}; // 活动的开始时间数组
int f[] = {2, 4, 6, 7, 9}; // 活动的结束时间数组
int A[n]; // 用于记录选择结果的数组
GreedySelector(n, s, f, A);
printf("选择的活动序号:");
for (int i = 0; i < n; i++) {
if (A[i] == 1) {
printf("%d ", i + 1);
}
}
printf("\n");
return 0;
}
代码解释:
在这个例子中,我们假设有 5 个活动,开始时间和结束时间分别用数组 s[]
和 f[]
表示。A[]
是用于记录选择结果的数组,其中元素为 1 表示选择该活动,为 0 表示不选择该活动。
在 main()
函数中,我们调用 GreedySelector()
函数,传递活动数量 n
、开始时间数组 s[]
、结束时间数组 f[]
和结果数组 A[]
。然后打印选择的活动序号,即结果数组中值为 1 的索引加 1。
这个例子中的输入数据和结果是固定的,您可以根据需要修改或扩展输入数据。请确保输入的活动已按照结束时间的非递减顺序排列,这样贪心算法才能正确工作。
C++:
#include <iostream>
#include <vector>
void GreedySelector(int n, std::vector<int>& s, std::vector<int>& f, std::vector<bool>& A) {
A[0] = true; // 第一个活动总是选中
int j = 0; // 记录最近一次加入到 A 中的活动的索引
for (int i = 1; i < n; i++) {
if (s[i] >= f[j]) {
A[i] = true; // 选择活动 i
j = i;
} else {
A[i] = false; // 不选择活动 i
}
}
}
int main() {
int n = 5; // 活动数量
std::vector<int> s = {1, 3, 0, 5, 8}; // 活动的开始时间数组
std::vector<int> f = {2, 4, 6, 7, 9}; // 活动的结束时间数组
std::vector<bool> A(n); // 用于记录选择结果的数组
GreedySelector(n, s, f, A);
std::cout << "选择的活动序号:";
for (int i = 0; i < n; i++) {
if (A[i]) {
std::cout << i + 1 << " ";
}
}
std::cout << std::endl;
return 0;
}
这段代码与之前的 C 代码实现基本相同,只是使用了 C++ 的容器 std::vector
来代替数组,并使用了 std::cout
和 std::endl
来进行输出。
在 main()
函数中,我们定义了活动数量 n
,开始时间数组 s
,结束时间数组 f
,以及用于记录选择结果的布尔型向量 A
。然后调用 GreedySelector()
函数,传递这些参数,并在屏幕上打印选择的活动序号。
这个例子中的输入数据和结果是固定的,您可以根据需要修改或扩展输入数据。请确保输入的活动已按照结束时间的非递减顺序排列,这样贪心算法才能正确工作。