贪心法是一种算法范式,它逐个构建解决方案,始终选择下一个提供最明显和最直接好处的部分。贪心算法用于优化问题。
如果问题具有以下属性,则可以使用 Greedy 解决优化问题:
- 在每一步中,我们都可以做出当前看起来最好的选择,从而得到完整问题的最优解。
如果贪心算法可以解决问题,那么它通常会成为解决该问题的最佳方法,因为贪心算法通常比动态规划等其他技术更有效。但是贪心算法并不总是适用的。例如,Fractional Knapsack问题可以使用 Greedy 来解决,但是0-1 Knapsack 问题不能使用 Greedy 来解决。
以下是贪心算法的一些标准算法:
1)Kruskal 的最小生成树(MST):
在 Kruskal 算法中,我们通过一条一条地挑选边来创建 MST。Greedy Choice 是在目前构建的 MST 中选取不会引起循环的最小权重边
2) Prim 的最小生成树:
同样在 Prim 的算法中,我们通过一条一条地挑选边缘来创建 MST。我们维护两个集合:一组已经包含在 MST 中的顶点和一组尚未包含的顶点。Greedy Choice 是选择连接两个集合的最小权重边
3) Dijkstra 的最短路径:
Dijkstra 算法与 Prim 算法非常相似。最短路径树是逐边构建的。我们维护两个集合:一组已经包含在树中的顶点和一组尚未包含的顶点。贪婪选择是选择连接两个集合的边,并且在从源到包含尚未包含的顶点的集合的最小权重路径上
4)霍夫曼编码:
霍夫曼编码是一种无损压缩技术。它将可变长度的位代码分配给不同的字符。Greedy Choice 是将最少位长度的代码分配给出现频率最高的字符。
贪心算法有时也用于获得 Hard 优化问题的近似值。例如,旅行商问题是一个 NP-Hard 问题。这个问题的贪心选择是每一步都从当前城市中选择最近的未访问过的城市。这些解决方案并不总是产生最佳最优解,但可用于获得近似最优解。
在这里让我们看一个可以使用贪心算法解决的问题
问题:
你有n 个活动及其开始和结束时间。选择一个人可以执行的最大活动数,假设一个人一次只能从事一项活动。
例子:
输入: start[] = {10, 12, 20}, finish[] = {20, 25, 30}
输出: 0 2
解释:一个人最多可以进行两项活动。
可以执行的最大活动集 是
{0, 2} [这些是 start[] 和 finish[] 中的索引]输入:开始[] = {1, 3, 0, 5, 8, 5},完成[] = {2, 4, 6, 7, 9, 9};
输出: 0 1 3 4
解释:一个人最多可以进行四项活动。
可以执行的最大活动集 是
{0, 1, 3, 4} [这些是 start[] 和 finish[] 中的索引
方法:要解决问题,请遵循以下想法:
贪心选择总是选择剩余活动中完成时间最短且开始时间大于或等于先前选择的活动的结束时间的下一个活动。我们可以根据活动的完成时间对活动进行排序,以便我们始终将下一个活动视为完成时间最短的活动
按照给定的步骤解决问题:
- 根据完成时间对活动进行排序
- 从排序的数组中选择第一个活动并打印它
- 对排序数组中的剩余活动执行以下操作
- 如果此活动的开始时间大于或等于先前选择的活动的结束时间,则选择此活动并打印它
注意:在实现中,假设活动已经按照完成时间排序
下面是上述方法的实现。
// C++ program for activity selection problem.
// The following implementation assumes that the activities
// are already sorted according to their finish time
#include <bits/stdc++.h>
using namespace std;
// Prints a maximum set of activities that can be done by a
// single person, one at a time.
void printMaxActivities(int s[], int f[], int n)
{
int i, j;
cout << "Following activities are selected" << endl;
// The first activity always gets selected
i = 0;
cout << i << " ";
// Consider rest of the activities
for (j = 1; j < n; j++) {
// If this activity has start time greater than or
// equal to the finish time of previously selected
// activity, then select it
if (s[j] >= f[i]) {
cout << j << " ";
i = j;
}
}
}
// Driver code
int main()
{
int s[] = { 1, 3, 0, 5, 8, 5 };
int f[] = { 2, 4, 6, 7, 9, 9 };
int n = sizeof(s) / sizeof(s[0]);
// Function call
printMaxActivities(s, f, n);
return 0;
}
// this code contributed by shivanisinghss2110
Following activities are selected
0 1 3 4
时间复杂度: O(N)
辅助空间: O(1)
贪婪选择如何适用于根据完成时间排序的活动?
设给定的活动集为 S = {1, 2, 3, …n},活动按完成时间排序。贪婪的选择是总是选择活动 1。为什么活动 1 总是提供最优解之一?
我们可以证明,如果存在第一个活动不是 1 的另一个解决方案 B,那么也存在与第一个活动相同大小的活动 1 的解决方案 A。设 B 选择的第一个活动为 k,则总存在 A = {B – {k}} U {1}。
注: B 中的活动是独立的,k 的完成时间是所有活动中最小的。因为 k 不为 1,所以 finish(k) >= finish(1))
当给定的活动未排序时如何实施?
我们为活动创建一个结构/类。我们按完成时间对所有活动进行排序。一旦我们对活动进行排序,我们就应用相同的算法。
下图是上述方法的说明:
下面是上述方法的实现:
// C++ program for activity selection problem
// when input activities may not be sorted.
#include <bits/stdc++.h>
using namespace std;
// A job has a start time, finish time and profit.
struct Activitiy {
int start, finish;
};
// A utility function that is used for sorting
// activities according to finish time
bool activityCompare(Activitiy s1, Activitiy s2)
{
return (s1.finish < s2.finish);
}
// Returns count of the maximum set of activities that can
// be done by a single person, one at a time.
void printMaxActivities(Activitiy arr[], int n)
{
// Sort jobs according to finish time
sort(arr, arr + n, activityCompare);
cout << "Following activities are selected :\n";
// The first activity always gets selected
int i = 0;
cout << "(" << arr[i].start << ", " << arr[i].finish
<< ")";
// Consider rest of the activities
for (int j = 1; j < n; j++) {
// If this activity has start time greater than or
// equal to the finish time of previously selected
// activity, then select it
if (arr[j].start >= arr[i].finish) {
cout << ", (" << arr[j].start << ", "
<< arr[j].finish << ")";
i = j;
}
}
}
// Driver code
int main()
{
Activitiy arr[] = { { 5, 9 }, { 1, 2 }, { 3, 4 },
{ 0, 6 }, { 5, 7 }, { 8, 9 } };
int n = sizeof(arr) / sizeof(arr[0]);
// Function call
printMaxActivities(arr, n);
return 0;
}
Following activities are selected :
(1, 2), (3, 4), (5, 7), (8, 9)
时间复杂度: O(N log N),如果输入活动可能无法排序。如果输入活动总是排序,则需要 O(n) 时间。
辅助空间: O(1)
使用优先级队列的活动选择问题:
我们可以使用 Min-Heap 来获得完成时间最短的活动。Min-Heap可以使用优先级队列来实现
按照给定的步骤解决问题:
- 创建一个优先级队列(Min-Heap)并将活动推送到其中。
- 将优先级队列的顶部推入答案向量并将变量start设置为第一个活动的开始时间,将end设置为活动的结束时间
- 当 priority 不为空时,请执行以下操作:
- 取优先级队列的顶部并检查
- 如果此活动的开始时间大于或等于最后选择的活动的结束时间,则将此活动推入答案向量
- 否则忽略它
- 打印选择的活动,存储在答案向量中
下面是上述方法的实现:
// C++ program for activity selection problem
// when input activities may not be sorted.
#include <bits/stdc++.h>
using namespace std;
void SelectActivities(vector<int> s, vector<int> f)
{
// Vector to store results.
vector<pair<int, int> > ans;
// Minimum Priority Queue to sort activities in
// ascending order of finishing time (f[i]).
priority_queue<pair<int, int>, vector<pair<int, int> >,
greater<pair<int, int> > >
p;
for (int i = 0; i < s.size(); i++) {
// Pushing elements in priority queue where the key
// is f[i]
p.push(make_pair(f[i], s[i]));
}
auto it = p.top();
int start = it.second;
int end = it.first;
p.pop();
ans.push_back(make_pair(start, end));
while (!p.empty()) {
auto itr = p.top();
p.pop();
if (itr.second >= end) {
start = itr.second;
end = itr.first;
ans.push_back(make_pair(start, end));
}
}
cout << "Following Activities should be selected. "
<< endl
<< endl;
for (auto itr = ans.begin(); itr != ans.end(); itr++) {
cout << "Activity started at " << (*itr).first
<< " and ends at " << (*itr).second << endl;
}
}
// Driver code
int main()
{
vector<int> s = { 1, 3, 0, 5, 8, 5 };
vector<int> f = { 2, 4, 6, 7, 9, 9 };
// Function call
SelectActivities(s, f);
return 0;
}
Following Activities should be selected。 Activity started at :1 and ends at 2 Activity started at :3 and ends at 4 Activity started at :5 and ends at 7 Activity started at :8 and ends at 9
时间复杂度: O(N * log N)
辅助空间: O(N)