贪心算法(Greedy Algorithm)是一种在每一步选择中都采取当前状态下最好或最优(即最有利)的选择,目的是希望通过每一步的局部最优选择最终达到全局最优解的一种算法设计方法。贪心算法的特点是简单、直观,通常用来解决一些优化问题。
贪心算法的基本步骤
1. 建立数学模型:将要解决的问题抽象为数学模型。
2. 设计贪心策略:确定能够在当前步骤选择中取得局部最优解的策略。
3. 证明贪心选择性质:证明通过每一步的贪心选择,最终可以得到全局最优解。
4. 设计算法并实现:根据贪心策略,设计并实现算法。
经典贪心算法例子
1. 活动选择问题(Activity Selection Problem):
问题描述:给定一组活动,每个活动都有开始时间和结束时间,选择尽可能多的活动且这些活动不重叠。
贪心策略:总是选择结束时间最早的活动。
#include <iostream>
#include <vector>
#include <algorithm>
// 定义活动结构体
struct Activity {
int start; // 活动开始时间
int end; // 活动结束时间
};
// 按结束时间排序的比较函数
bool activityCompare(Activity a, Activity b) {
return a.end < b.end;
}
void activitySelection(std::vector<Activity>& activities) {
// 按结束时间排序
std::sort(activities.begin(), activities.end(), activityCompare);
int n = activities.size();
std::cout << "Selected activities: ";
// 第一个活动总是被选择
int lastSelectedIndex = 0;
std::cout << "(" << activities[lastSelectedIndex].start << ", " << activities[lastSelectedIndex].end << ")";
// 选择剩余的活动
for (int i = 1; i < n; i++) {
// 如果当前活动的开始时间不早于上一个选择的活动的结束时间,则选择该活动
if (activities[i].start >= activities[lastSelectedIndex].end) {
std::cout << ", (" << activities[i].start << ", " << activities[i].end << ")";
lastSelectedIndex = i;
}
}
std::cout << std::endl;
}
int main() {
// 初始化活动列表,每个活动包含开始时间和结束时间
std::vector<Activity> activities = {{5, 9}, {1, 2}, {3, 4}, {0, 6}, {5, 7}, {8, 9}};
// 调用活动选择函数
activitySelection(activities);
return 0;
}
2. 背包问题(Knapsack Problem):
问题描述:有一个容量固定的背包和一组物品,每个物品都有价值和重量,选择放入背包的物品使得总价值最大。
贪心策略(分数背包问题):总是选择单位重量价值最高的物品。
#include <iostream>
#include <vector>
#include <algorithm>
// 定义物品结构体
struct Item {
int value; // 物品的价值
int weight; // 物品的重量
};
// 按单位重量价值排序的比较函数
bool compare(Item a, Item b) {
double r1 = (double)a.value / a.weight;
double r2 = (double)b.value / b.weight;
return r1 > r2;
}
double fractionalKnapsack(int W, std::vector<Item>& items) {
// 按单位重量价值排序
std::sort(items.begin(), items.end(), compare);
int n = items.size();
double totalValue = 0.0;
for (int i = 0; i < n; i++) {
// 如果当前物品重量小于或等于背包剩余容量,则全部放入背包
if (items[i].weight <= W) {
W -= items[i].weight;
totalValue += items[i].value;
} else {
// 如果当前物品重量大于背包剩余容量,则放入部分物品
totalValue += items[i].value * ((double)W / items[i].weight);
break;
}
}
return totalValue;
}
int main() {
int W = 50; // 背包容量
// 初始化物品列表,每个物品包含价值和重量
std::vector<Item> items = {{60, 10}, {100, 20}, {120, 30}};
// 调用分数背包函数
double maxValue = fractionalKnapsack(W, items);
std::cout << "Maximum value in Knapsack = " << maxValue << std::endl;
return 0;
}
3. 哈夫曼编码(Huffman Coding):
问题描述:给定一组字符及其出现频率,设计最优的编码方案使得编码后的总长度最短。
贪心策略:总是合并频率最小的两个节点。
#include <iostream>
#include <queue>
#include <vector>
// 定义最小堆节点结构体
struct MinHeapNode {
char data; // 字符
unsigned freq; // 字符频率
MinHeapNode *left, *right; // 左右子节点
// 构造函数
MinHeapNode(char data, unsigned freq) {
left = right = nullptr;
this->data = data;
this->freq = freq;
}
};
// 比较函数,用于构建最小堆
struct compare {
bool operator()(MinHeapNode* left, MinHeapNode* right) {
return (left->freq > right->freq);
}
};
// 打印哈夫曼编码
void printCodes(struct MinHeapNode* root, std::string str) {
if (!root)
return;
// 如果是叶子节点,打印字符和它的编码
if (root->data != '$')
std::cout << root->data << ": " << str << "\n";
// 递归调用,处理左子树和右子树
printCodes(root->left, str + "0");
printCodes(root->right, str + "1");
}
// 构建哈夫曼树并打印编码
void HuffmanCodes(char data[], int freq[], int size) {
struct MinHeapNode *left, *right, *top;
// 创建一个优先队列(最小堆)
std::priority_queue<MinHeapNode*, std::vector<MinHeapNode*>, compare> minHeap;
// 将所有字符和它们的频率插入到优先队列中
for (int i = 0; i < size; ++i)
minHeap.push(new MinHeapNode(data[i], freq[i]));
// 直到堆中只剩下一个节点
while (minHeap.size() != 1) {
// 取出频率最小的两个节点
left = minHeap.top();
minHeap.pop();
right = minHeap.top();
minHeap.pop();
// 创建一个新的内部节点,其频率是这两个节点频率之和
top = new MinHeapNode('$', left->freq + right->freq);
top->left = left;
top->right = right;
// 将新的节点插入到堆中
minHeap.push(top);
}
// 打印哈夫曼编码
printCodes(minHeap.top(), "");
}
int main() {
// 初始化字符数组和对应的频率数组
char arr[] = {'a', 'b', 'c', 'd', 'e', 'f'};
int freq[] = {5, 9, 12, 13, 16, 45};
int size = sizeof(arr) / sizeof(arr[0]);
// 构建哈夫曼树并打印编码
HuffmanCodes(arr, freq, size);
return 0;
}
贪心算法的优缺点
优点:
简单且直观,易于实现。
通常在求解某些问题时,能够提供高效的解法。
缺点:
不能保证所有问题都能得到最优解。
适用于具有贪心选择性质的问题,如果问题不满足贪心选择性质,贪心算法可能无法得到最优解。
总的来说,贪心算法是一种强大且高效的算法设计方法,在适当的问题上可以快速找到优良的解决方案。