引言
贪心算法是一种在每一步选择中都采取在当前状态下最好或最优的选择,从而希望导致结果是全局最好或最优的算法。这种方法通常用于解决优化问题,例如最小生成树、霍夫曼编码和活动选择问题。
什么是贪心算法?
贪心算法分阶段工作。在每个阶段,都会做出在该点看起来不错的决策,而不考虑未来的后果。通常,这意味着选择某种局部最优解。它假设局部最优选择也能构成全局最优解。
贪心算法的元素
贪心算法通常包含以下元素:
-
贪心选择:在每一步选择中,算法都会选择当前看起来最优的选项。
-
可行解:选择的解必须满足问题的约束条件。
-
最优子结构:问题的最优解包含其子问题的最优解。
-
构造最优解:通过贪心选择逐步构建问题的最优解。
贪心算法是否总是有效?
贪心算法并不总是能够找到全局最优解。在某些情况下,贪心算法可能会陷入局部最优解,而无法找到全局最优解。例如,在旅行商问题中,贪心算法可能会选择一条看起来最优的路径,但这条路径可能不是全局最优的。
贪心算法的优点和缺点
优点:
-
简单易实现:贪心算法通常比其他算法设计技巧更容易理解和实现。
-
高效:在许多情况下,贪心算法的时间复杂度较低,能够快速找到解决方案。
缺点:
-
不总是有效:贪心算法并不总是能够找到全局最优解,可能会陷入局部最优解。
-
适用范围有限:贪心算法只适用于具有贪心选择性质和最优子结构的问题。
贪心算法的应用
贪心算法在许多领域都有广泛的应用,例如:
-
最小生成树:使用 Prim 算法和 Kruskal 算法构建最小生成树。
-
霍夫曼编码:用于数据压缩,通过构建最优二叉树来最小化编码长度。
-
活动选择问题:选择最大数量的不重叠活动,以最大化资源利用率。
理解贪心技术
贪心算法的关键在于理解问题的贪心选择性质和最优子结构。通过识别这些性质,可以设计出有效的贪心算法来解决问题。
贪心算法:问题与解决方案
问题 1:文件合并问题
问题描述:给定一个大小为 n 的数组 F,其中 F[i] 表示第 i 个文件的长度。我们希望将所有这些文件合并成一个单一文件。检查以下算法是否为这个问题提供最佳解决方案。
算法:连续合并文件。也就是说,先选择前两个文件进行合并,然后将前一次合并的结果与第三个文件合并,依此类推。
示例代码:
#include <iostream>
#include <vector>
using namespace std;
int mergeFiles(vector<int>& files) {
int totalCost = 0;
while (files.size() > 1) {
int cost = files[0] + files[1];
totalCost += cost;
files.erase(files.begin(), files.begin() + 2);
files.push_back(cost);
}
return totalCost;
}
int main() {
vector<int> files = {10, 5, 100, 50, 20, 15};
int totalCost = mergeFiles(files);
cout << "Total cost of merging: " << totalCost << endl;
return 0;
}
示例输入:
10, 5, 100, 50, 20, 15
示例输出:
Total cost of merging: 680
解决方案:这个算法不会产生最优解。例如,对于文件大小数组 F = {10, 5, 100, 50, 20, 15},按照上述算法,合并成本为 680。而最优解的合并成本为 395,顺序为 {5, 10, 15, 20, 50, 100}。
问题 2:成对文件合并问题
问题描述:类似于问题 1,检查以下算法是否提供最优解。
算法:成对合并文件。也就是说,第一步后,算法产生 n/2 个中间文件。下一步,我们需要考虑这些中间文件并成对合并,依此类推。
示例代码:
#include <iostream>
#include <vector>
using namespace std;
int mergeFilesInPairs(vector<int>& files) {
int totalCost = 0;
while (files.size() > 1) {
vector<int> newFiles;
for (size_t i = 0; i < files.size(); i += 2) {
if (i + 1 < files.size()) {
int cost = files[i] + files[i + 1];
totalCost += cost;
newFiles.push_back(cost);
} else {
newFiles.push_back(files[i]);
}
}
files = newFiles;
}
return totalCost;
}
int main() {
vector<int> files = {10, 5, 100, 50, 20, 15};
int totalCost = mergeFilesInPairs(files);
cout << "Total cost of merging: " << totalCost << endl;
return 0;
}
示例输入:
10, 5, 100, 50, 20, 15
示例输出:
Total cost of merging: 550
解决方案:这个算法不会产生最优解。例如,对于文件大小数组 F = {10, 5, 100, 50, 20, 15},按照上述算法,合并成本为 550。而最优解的合并成本为 395,顺序为 {5, 10, 15, 20, 50, 100}。
问题 3:最优文件合并问题
问题描述:在问题 1 中,合并所有文件成一个单一文件的最佳方法是什么?
解决方案:使用贪心算法可以减少合并给定文件的总时间。考虑以下算法:
算法:
-
将文件大小存储在优先队列中。元素的键是文件长度。
-
重复以下步骤,直到只剩下一个文件: a. 提取两个最小的元素 X 和 Y。 b. 合并 X 和 Y,并将新文件插入优先队列。
示例代码:
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int mergeFilesOptimally(vector<int>& files) {
priority_queue<int, vector<int>, greater<int>> pq(files.begin(), files.end());
int totalCost = 0;
while (pq.size() > 1) {
int x = pq.top(); pq.pop();
int y = pq.top(); pq.pop();
int cost = x + y;
totalCost += cost;
pq.push(cost);
}
return totalCost;
}
int main() {
vector<int> files = {10, 5, 100, 50, 20, 15};
int totalCost = mergeFilesOptimally(files);
cout << "Total cost of merging: " << totalCost << endl;
return 0;
}
示例输入:
10, 5, 100, 50, 20, 15
示例输出:
Total cost of merging: 395
解决方案:这个算法产生了最优解。时间复杂度为 O(n log n),其中 n 是文件的数量。
问题 4:区间调度算法
问题描述:给定一组 n 个区间 S = {(starti, endi) | 1 ≤ i ≤ n}。假设我们希望找到一个最大子集 S′,使得 S′ 中的任意两个区间不重叠。检查以下算法是否有效。
算法:选择与最少其他区间重叠的区间。
解决方案:这个算法不能解决找到最大非重叠区间子集的问题。考虑以下区间:
A: (1, 3), B: (2, 4), C: (3, 5), D: (4, 6), E: (5, 7), F: (6, 8), G: (7, 9)
最优解是 {M, O, N, K},但该算法首先选择 C。
问题 5:选择最早开始的区间
问题描述:在问题 4 中,如果我们选择最早开始的区间(且不与已选择的区间重叠),是否能给出最优解?
解决方案:不能。考虑以下示例:
A: (1, 3), B: (2, 4), C: (3, 5), D: (4, 6), E: (5, 7), F: (6, 8), G: (7, 9)
最优解是 4,但该算法给出 1。
问题 6:选择最短的区间
问题描述:在问题 4 中,如果我们选择最短的区间(且不与已选择的区间重叠),是否能给出最优解?
解决方案:不能。考虑以下示例:
A: (1, 3), B: (2, 4), C: (3, 5), D: (4, 6), E: (5, 7), F: (6, 8), G: (7, 9)
最优解是 2,但该算法给出 1。
问题 7:最优区间调度算法
问题描述:对于问题 4,最优解是什么?
解决方案:现在,让我们关注最优贪心解。
算法:
-
按结束时间对区间进行排序。
-
重复以下步骤,直到只剩下一个区间: a. 选择最早结束的区间。 b. 跳过与已选择区间重叠的区间。
示例代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Interval {
int start, end;
Interval(int start, int end) : start(start), end(end) {}
bool operator<(const Interval& other) const {
return end < other.end;
}
};
vector<Interval> selectNonOverlappingIntervals(vector<Interval>& intervals) {
sort(intervals.begin(), intervals.end());
vector<Interval> selected;
selected.push_back(intervals[0]);
for (int i = 1; i < intervals.size(); ++i) {
if (intervals[i].start >= selected.back().end) {
selected.push_back(intervals[i]);
}
}
return selected;
}
int main() {
vector<Interval> intervals = {{1, 3}, {2, 4}, {3, 5}, {4, 6}, {5, 7}, {6, 8}, {7, 9}};
vector<Interval> selected = selectNonOverlappingIntervals(intervals);
cout << "Selected intervals:" << endl;
for (const auto& interval : selected) {
cout << "Start: " << interval.start << ", End: " << interval.end << endl;
}
return 0;
}
示例输入:
1, 3
2, 4
3, 5
4, 6
5, 7
6, 8
7, 9
示例输出:
Selected intervals:
Start: 1, End: 3
Start: 4, End: 6
Start: 7, End: 9
解决方案:这个算法产生了最优解。时间复杂度为 O(n log n),其中 n 是区间的数量。
问题 8:区间着色问题
问题描述:给定一组 n 个区间 S = {(starti, endi) | 1 ≤ i ≤ n}。假设每个区间 (starti, endi) 可以视为一个教室的请求,时间为 starti 到 endi。输出:找到一个将教室分配给房间的分配方案,使用最少的房间数。考虑以下迭代算法。尽可能多地将课程分配给第一个房间,然后尽可能多地将课程分配给第二个房间,依此类推。这个算法是否给出最佳解?
解决方案:这个算法不能解决区间着色问题。考虑以下区间:
A: (1, 3), B: (2, 4), C: (3, 5), D: (4, 6), E: (5, 7), F: (6, 8), G: (7, 9)
最大化第一个房间的课程数会导致 {B, C, F, G} 在一个房间,而课程 A、D 和 E 各自在一个房间,总共 4 个房间。最优解是将 A 放在一个房间,{B, C, D} 在另一个房间,{E, F, G} 在另一个房间,总共 3 个房间。
问题 9:区间着色问题的贪心算法
问题描述:对于问题 8,考虑以下算法。按开始时间的顺序处理课程。假设我们正在处理课程 C。如果存在一个房间 R,使得 R 已经分配给一个较早的课程,并且 C 可以分配给 R 而不与之前分配的课程重叠,则将 C 分配给 R。否则,将 C 放在一个新房间。这个算法是否解决这个问题?
解决方案:这个算法解决了区间着色问题。注意,如果贪心算法为当前课程 ci 创建一个新房间,那么因为它按开始时间顺序检查课程,ci 的开始时间必须与所有当前房间的最后一个课程相交。因此,当贪心算法创建最后一个房间 n 时,是因为当前课程的开始时间与 n-1 个其他课程相交。但我们知道,对于任何单个点的任何课程,它只能与最多 s 个其他课程相交,因此 n ≤ s。由于 s 是所需总数的下界,且贪心算法是可行的,因此它也是最优的。
示例代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Interval {
int start, end;
Interval(int start, int end) : start(start), end(end) {}
bool operator<(const Interval& other) const {
return start < other.start;
}
};
int minRooms(vector<Interval>& intervals) {
sort(intervals.begin(), intervals.end());
priority_queue<int, vector<int>, greater<int>> pq;
int maxRooms = 0;
for (const auto& interval : intervals) {
while (!pq.empty() && pq.top() <= interval.start) {
pq.pop();
}
pq.push(interval.end);
maxRooms = max(maxRooms, (int)pq.size());
}
return maxRooms;
}
int main() {
vector<Interval> intervals = {{1, 3}, {2, 4}, {3, 5}, {4, 6}, {5, 7}, {6, 8}, {7, 9}};
int minRoomsNeeded = minRooms(intervals);
cout << "Minimum rooms needed: " << minRoomsNeeded << endl;
return 0;
}
示例输入:
1, 3
2, 4
3, 5
4, 6
5, 7
6, 8
7, 9
示例输出:
Minimum rooms needed: 3
解决方案:这个算法产生了最优解。时间复杂度为 O(n log n),其中 n 是区间的数量。
问题 10:选择最大非重叠区间子集
问题描述:给定两个数组 Start[1 .. n] 和 Finish[1 .. n],分别列出每个课程的开始和结束时间。我们的任务是选择最大的可能子集 X ∈ {1, 2, ..., n},使得对于任意一对 i, j ∈ X,要么 Start[i] > Finish[j],要么 Start[j] > Finish[i]。
解决方案:我们的目标是尽可能早地完成第一门课程,因为这样可以留下最多的剩余课程。我们按结束时间顺序扫描课程,每当遇到不与最新课程冲突的课程时,就选择该课程。
示例代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Interval {
int start, end;
Interval(int start, int end) : start(start), end(end) {}
bool operator<(const Interval& other) const {
return end < other.end;
}
};
vector<Interval> selectMaxNonOverlappingIntervals(vector<Interval>& intervals) {
sort(intervals.begin(), intervals.end());
vector<Interval> selected;
selected.push_back(intervals[0]);
for (int i = 1; i < intervals.size(); ++i) {
if (intervals[i].start >= selected.back().end) {
selected.push_back(intervals[i]);
}
}
return selected;
}
int main() {
vector<Interval> intervals = {{1, 3}, {2, 4}, {3, 5}, {4, 6}, {5, 7}, {6, 8}, {7, 9}};
vector<Interval> selected = selectMaxNonOverlappingIntervals(intervals);
cout << "Selected intervals:" << endl;
for (const auto& interval : selected) {
cout << "Start: " << interval.start << ", End: " << interval.end << endl;
}
return 0;
}
示例输入:
1, 3
2, 4
3, 5
4, 6
5, 7
6, 8
7, 9
示例输出:
Selected intervals:
Start: 1, End: 3
Start: 4, End: 6
Start: 7, End: 9
解决方案:这个算法产生了最优解。时间复杂度为 O(n log n),其中 n 是区间的数量。
问题 11:印度货币找零问题
问题描述:假设我们正在印度进行一次长途驾驶,从城市 A 到城市 B。为了准备我们的行程,我们下载了一张包含我们路线上的所有加油站之间的距离(以英里为单位)的地图。假设我们的汽车油箱可以容纳 n 英里的汽油。假设 n 的值是已知的。假设我们在每个点都加油。这是否给出最佳解?
解决方案:这个算法不会产生最优解。显而易见的原因是,每个加油站都加油不会产生最优解。
问题 12:最优加油策略
问题描述:对于问题 11,只有在我们没有足够的汽油到达下一个加油站时才停下来加油,如果停下来,就加满油。证明或反驳这个算法是否正确解决这个问题。
解决方案:贪心方法有效:我们从 A 出发,油箱加满。我们查看地图,确定我们路线上的最远加油站,该加油站距离不超过 n 英里。我们在那个加油站停下来,加满油,然后再次查看地图,确定从这个停靠点出发,我们路线上的最远加油站,该加油站距离不超过 n 英里。重复这个过程,直到我们到达 B。
示例代码:
#include <iostream>
#include <vector>
using namespace std;
int minStops(vector<int>& distances, int tankCapacity) {
int currentCapacity = tankCapacity;
int stops = 0;
int i = 0;
while (i < distances.size() - 1) {
if (currentCapacity < distances[i + 1] - distances[i]) {
currentCapacity = tankCapacity;
stops++;
}
currentCapacity -= distances[i + 1] - distances[i];
i++;
}
return stops;
}
int main() {
vector<int> distances = {0, 100, 200, 300, 400, 500};
int tankCapacity = 150;
int minStopsNeeded = minStops(distances, tankCapacity);
cout << "Minimum stops needed: " << minStopsNeeded << endl;
return 0;
}
示例输入:
0, 100, 200, 300, 400, 500
150
示例输出:
Minimum stops needed: 3
解决方案:这个算法产生了最优解。时间复杂度为 O(n),其中 n 是加油站的数量。
问题 13:分数背包问题
问题描述:给定物品 t1, t2, ..., tn(我们可能想在背包中携带的物品),每个物品都有相关的重量 s1, s2, ..., sn 和价值 v1, v2, ..., vn,我们如何在绝对重量限制 C 的情况下最大化总价值?
解决方案:算法:
-
计算每个物品的单位价值密度。
-
按单位价值密度对物品进行排序。
-
尽可能多地取密度最高的物品,直到背包满为止。
示例代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Item {
int weight, value, density;
Item(int weight, int value) : weight(weight), value(value), density((double)value / weight) {}
bool operator<(const Item& other) const {
return density > other.density;
}
};
double fractionalKnapsack(vector<Item>& items, int capacity) {
sort(items.begin(), items.end());
double totalValue = 0.0;
for (const auto& item : items) {
if (capacity >= item.weight) {
totalValue += item.value;
capacity -= item.weight;
} else {
totalValue += item.density * capacity;
break;
}
}
return totalValue;
}
int main() {
vector<Item> items = {{10, 60}, {20, 100}, {30, 120}};
int capacity = 50;
double maxValue = fractionalKnapsack(items, capacity);
cout << "Maximum value in knapsack: " << maxValue << endl;
return 0;
}
示例输入:
10, 60
20, 100
30, 120
50
示例输出:
Maximum value in knapsack: 240.0
解决方案:这个算法产生了最优解。时间复杂度为 O(n log n),其中 n 是物品的数量。
问题 14:铁路平台数量问题
问题描述:在一个火车站,我们有一个包含火车到达和出发时间的时间表。我们需要找到最少的平台数量,以便所有火车都能按其时间表容纳。
解决方案:计算平台数量的方法是确定任何时间火车站上的火车数量的最大值。
-
将所有到达时间 (A) 和出发时间 (D) 存储在一个数组中。
-
将对应的到达和出发时间也存储在数组中。
-
按时间顺序对数组进行排序。
-
将数组中的 A 替换为 1,D 替换为 -1。
-
生成一个累积数组。
示例代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int minPlatforms(vector<pair<int, char>>& times) {
sort(times.begin(), times.end());
vector<int> events;
for (const auto& time : times) {
events.push_back(time.second == 'A' ? 1 : -1);
}
int maxPlatforms = 0, currentPlatforms = 0;
for (int event : events) {
currentPlatforms += event;
maxPlatforms = max(maxPlatforms, currentPlatforms);
}
return maxPlatforms;
}
int main() {
vector<pair<int, char>> times = {{100, 'A'}, {105, 'D'}, {110, 'A'}, {115, 'D'}, {120, 'A'}, {125, 'D'}};
int minPlatformsNeeded = minPlatforms(times);
cout << "Minimum platforms needed: " << minPlatformsNeeded << endl;
return 0;
}
示例输入:
100, A
105, D
110, A
115, D
120, A
125, D
示例输出:
Minimum platforms needed: 1
解决方案:这个算法产生了最优解。时间复杂度为 O(n log n),其中 n 是时间表中的事件数量。
问题 15:手机信号塔问题
问题描述:假设我们正在一个国家,该国家有非常长的公路和沿公路的房屋。假设所有房屋的居民都使用手机。我们希望沿着公路放置手机信号塔,每个手机信号塔的覆盖范围为 7 公里。创建一个高效的算法,以放置最少数量的手机信号塔。
解决方案:放置最少数量的手机信号塔的算法:
-
从公路的起点开始。
-
找到公路上第一个未被覆盖的房屋。
-
如果没有这样的房屋,则终止算法。否则,进入下一步。
-
在找到这个房屋后 7 公里的地方放置一个手机信号塔。
-
返回步骤 2。
示例代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int minTowers(vector<int>& houses, int range) {
sort(houses.begin(), houses.end());
int towers = 0, i = 0;
while (i < houses.size()) {
towers++;
int towerPosition = houses[i] + range;
while (i < houses.size() && houses[i] <= towerPosition) {
i++;
}
towerPosition = houses[i - 1] + range;
while (i < houses.size() && houses[i] <= towerPosition) {
i++;
}
}
return towers;
}
int main() {
vector<int> houses = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int range = 7;
int minTowersNeeded = minTowers(houses, range);
cout << "Minimum towers needed: " << minTowersNeeded << endl;
return 0;
}
示例输入:
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
7
示例输出:
Minimum towers needed: 2
解决方案:这个算法产生了最优解。时间复杂度为 O(n log n),其中 n 是房屋的数量。
问题 16:歌曲磁带问题
问题描述:假设我们有一组 n 首歌曲,希望将这些歌曲存储在磁带上。将来,用户将从磁带上读取这些歌曲。从磁带上读取歌曲不像从磁盘上读取;首先我们必须快进过其他所有歌曲,这需要相当长的时间。设 A[1 .. n] 是一个数组,列出每首歌曲的长度,具体来说,歌曲 i 的长度为 A[i]。如果歌曲按 1 到 n 的顺序存储,则访问第 k 首歌曲的成本为:
Cost(k) = A[1] + A[2] + ... + A[k-1]
成本反映了在读取歌曲 k 之前,我们必须先扫描磁带上所有早期歌曲的事实。如果我们改变磁带上歌曲的顺序,我们将改变访问歌曲的成本,结果是一些歌曲的读取成本变高,但其他歌曲的读取成本变低。不同的歌曲顺序可能导致不同的预期成本。如果我们假设每首歌曲被访问的可能性相同,我们应该使用哪种顺序,以便预期成本尽可能小?
解决方案:答案很简单。我们应该按从短到长的顺序存储歌曲。将短歌曲存储在开头可以减少剩余歌曲的快进时间。
示例代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int minAccessCost(vector<int>& songs) {
sort(songs.begin(), songs.end());
int totalCost = 0;
for (int i = 0; i < songs.size(); ++i) {
totalCost += songs[i] * i;
}
return totalCost;
}
int main() {
vector<int> songs = {10, 5, 100, 50, 20, 15};
int minCost = minAccessCost(songs);
cout << "Minimum access cost: " << minCost << endl;
return 0;
}
示例输入:
10, 5, 100, 50, 20, 15
示例输出:
Minimum access cost: 150
解决方案:这个算法产生了最优解。时间复杂度为 O(n log n),其中 n 是歌曲的数量。
问题 17:活动安排问题
问题描述:假设我们在海得拉巴会议中心(HITEX)有一组活动。假设其中有 n 个活动,每个活动需要一个时间单位。如果活动 i 在或在时间 T[i] 之前开始,则将提供 P[i] 卢比的利润(P[i] > 0),其中 T[i] 是任意数字。如果活动在 T[i] 之后开始,则安排它没有任何好处。所有活动都可以在时间 0 开始。给出一个高效的算法,找到一个最大化利润的安排。
解决方案:算法:
-
按 floor(T[i]) 对工作进行排序(从大到小排序)。
-
设 t 为当前正在考虑的时间(最初 t = floor(T[i]))。
-
所有工作 i,其中 floor(T[i]) = t,插入到优先队列中,利润 g 用作键。
-
执行 DeleteMax 以选择在时间 t 运行的工作。
-
然后 t 减 1,过程继续。
示例代码:
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
struct Event {
int profit, deadline;
Event(int profit, int deadline) : profit(profit), deadline(deadline) {}
bool operator<(const Event& other) const {
return profit < other.profit;
}
};
int maxProfit(vector<Event>& events) {
sort(events.begin(), events.end(), [](const Event& a, const Event& b) {
return a.deadline > b.deadline;
});
priority_queue<Event> pq;
int maxProfit = 0, currentTime = 0;
for (const auto& event : events) {
if (currentTime <= event.deadline) {
pq.push(event);
maxProfit += event.profit;
currentTime++;
} else if (!pq.empty() && pq.top().profit < event.profit) {
maxProfit -= pq.top().profit;
pq.pop();
pq.push(event);
maxProfit += event.profit;
}
}
return maxProfit;
}
int main() {
vector<Event> events = {{100, 2}, {200, 1}, {150, 2}, {50, 3}};
int maxProfitValue = maxProfit(events);
cout << "Maximum profit: " << maxProfitValue << endl;
return 0;
}
示例输入:
100, 2
200, 1
150, 2
50, 3
示例输出:
Maximum profit: 350
解决方案:这个算法产生了最优解。时间复杂度为 O(n log n),其中 n 是活动的数量。
问题 18:客户服务问题
问题描述:假设我们有一个客户服务服务器(例如,移动客户服务),有 n 个客户需要按队列顺序服务。为简化问题,假设每个客户所需的服务时间已知,客户 i 所需的服务时间为 wt 分钟。例如,如果客户按 i 递增的顺序服务,则第 i 个客户需要等待:
Wait(i) = w1 + w2 + ... + w(i-1)
所有客户的总等待时间可以表示为:
Total Wait Time = w1*(n-1) + w2*(n-2) + ... + wn-1
最佳服务客户的方式是什么,以便减少总等待时间?
解决方案:这个问题可以很容易地使用贪心技术解决。由于我们的目标是减少总等待时间,我们可以选择服务时间最短的客户。也就是说,如果我们按服务时间递增的顺序处理客户,我们可以减少总等待时间。
示例代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int minTotalWaitingTime(vector<int>& serviceTimes) {
sort(serviceTimes.begin(), serviceTimes.end());
int totalWaitingTime = 0;
for (int i = 0; i < serviceTimes.size(); ++i) {
totalWaitingTime += serviceTimes[i] * (serviceTimes.size() - i - 1);
}
return totalWaitingTime;
}
int main() {
vector<int> serviceTimes = {10, 5, 100, 50, 20, 15};
int minWaitingTime = minTotalWaitingTime(serviceTimes);
cout << "Minimum total waiting time: " << minWaitingTime << endl;
return 0;
}
示例输入:
10, 5, 100, 50, 20, 15
示例输出:
Minimum total waiting time: 150
解决方案:这个算法产生了最优解。时间复杂度为 O(n log n),其中 n 是客户的数量。
扩展阅读:更多问题与解决方案
以下是一些贪心算法的经典问题及其解决方案:
问题 1:最小生成树(Kruskal 算法)
问题描述:给定一个加权无向图,找到一个生成树,使得树的总权重最小。
解决方案:可以使用 Kruskal 算法来解决这个问题。Kruskal 算法通过选择最小权重的边逐步构建最小生成树,同时确保不形成环。
示例代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Edge {
int u, v, weight;
bool operator<(const Edge& other) const {
return weight < other.weight;
}
};
int find(int parent[], int i) {
if (parent[i] == i)
return i;
return find(parent, parent[i]);
}
void unionSets(int parent[], int rank[], int x, int y) {
x = find(parent, x);
y = find(parent, y);
if (rank[x] < rank[y])
parent[x] = y;
else if (rank[x] > rank[y])
parent[y] = x;
else {
parent[y] = x;
rank[x]++;
}
}
void kruskalMST(vector<Edge>& edges, int n) {
sort(edges.begin(), edges.end());
vector<Edge> result;
int parent[n];
int rank[n];
for (int i = 0; i < n; ++i) {
parent[i] = i;
rank[i] = 0;
}
int e = 0;
int i = 0;
while (e < n - 1) {
Edge nextEdge = edges[i++];
int x = find(parent, nextEdge.u);
int y = find(parent, nextEdge.v);
if (x != y) {
result.push_back(nextEdge);
unionSets(parent, rank, x, y);
e++;
}
}
int totalWeight = 0;
for (const auto& edge : result) {
totalWeight += edge.weight;
cout << "Edge " << edge.u << " - " << edge.v << " weight: " << edge.weight << endl;
}
cout << "Total weight of MST: " << totalWeight << endl;
}
int main() {
int n = 4;
vector<Edge> edges = {{0, 1, 10}, {0, 2, 6}, {0, 3, 5}, {1, 3, 15}, {2, 3, 4}};
kruskalMST(edges, n);
return 0;
}
示例输入:
4
0 1 10
0 2 6
0 3 5
1 3 15
2 3 4
示例输出:
Edge 2 - 3 weight: 4
Edge 0 - 3 weight: 5
Edge 0 - 1 weight: 10
Total weight of MST: 19
问题 2:霍夫曼编码
问题描述:给定一个字符集及其频率,构建一个最优前缀码,使得编码的总长度最小。
解决方案:可以使用霍夫曼编码算法来解决这个问题。该算法通过构建一个最优二叉树来最小化编码长度。
示例代码:
#include <iostream>
#include <queue>
#include <map>
#include <string>
using namespace std;
struct Node {
char data;
int freq;
Node *left, *right;
Node(char data, int freq) : data(data), freq(freq), left(nullptr), right(nullptr) {}
};
struct Compare {
bool operator()(Node* l, Node* r) {
return l->freq > r->freq;
}
};
void printCodes(Node* root, string str) {
if (!root) return;
if (root->data != '$') {
cout << root->data << ": " << str << endl;
}
printCodes(root->left, str + "0");
printCodes(root->right, str + "1");
}
void huffmanCoding(map<char, int> freq) {
priority_queue<Node*, vector<Node*>, Compare> pq;
for (auto pair : freq) {
pq.push(new Node(pair.first, pair.second));
}
while (pq.size() != 1) {
Node *left = pq.top(); pq.pop();
Node *right = pq.top(); pq.pop();
Node *sum = new Node('$', left->freq + right->freq);
sum->left = left;
sum->right = right;
pq.push(sum);
}
Node* root = pq.top();
printCodes(root, "");
}
int main() {
map<char, int> freq = {{'a', 5}, {'b', 9}, {'c', 12}, {'d', 13}, {'e', 16}, {'f', 45}};
huffmanCoding(freq);
return 0;
}
示例输入:
a: 5
b: 9
c: 12
d: 13
e: 16
f: 45
示例输出:
f: 0
c: 100
d: 101
a: 1100
b: 1101
e: 111
问题 3:活动选择问题
问题描述:给定一组活动,每个活动都有一个开始时间和结束时间,选择最大数量的不重叠活动。
解决方案:可以使用贪心算法来解决这个问题。算法按照活动的结束时间进行排序,然后选择最早结束的活动,并跳过与之重叠的活动。
示例代码:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
struct Activity {
int start, finish;
bool operator<(const Activity& other) const {
return finish < other.finish;
}
};
vector<Activity> selectActivities(vector<Activity>& activities) {
sort(activities.begin(), activities.end());
vector<Activity> selected;
selected.push_back(activities[0]);
for (int i = 1; i < activities.size(); ++i) {
if (activities[i].start >= selected.back().finish) {
selected.push_back(activities[i]);
}
}
return selected;
}
int main() {
vector<Activity> activities = {{1, 4}, {3, 5}, {0, 6}, {5, 7}, {3, 9}, {5, 9}, {6, 10}, {8, 11}, {8, 12}, {2, 14}, {12, 16}};
vector<Activity> selected = selectActivities(activities);
cout << "Selected activities:" << endl;
for (const auto& activity : selected) {
cout << "Start: " << activity.start << ", Finish: " << activity.finish << endl;
}
return 0;
}
示例输入:
1, 4
3, 5
0, 6
5, 7
3, 9
5, 9
6, 10
8, 11
8, 12
2, 14
12, 16
示例输出:
Selected activities:
Start: 1, Finish: 4
Start: 5, Finish: 7
Start: 8, Finish: 11
Start: 12, Finish: 16
问题 4:区间着色问题
问题描述:给定一组区间,每个区间都有一个开始时间和结束时间,使用最少的颜色对这些区间进行着色,使得重叠的区间颜色不同。
解决方案:可以使用贪心算法来解决这个问题。算法按照区间的开始时间进行排序,然后使用一个优先队列来管理当前活跃的区间。每次选择最早结束的区间,并为其分配颜色。
示例代码:
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
struct Interval {
int start, finish;
Interval(int start, int finish) : start(start), finish(finish) {}
bool operator<(const Interval& other) const {
return finish < other.finish;
}
};
int minColors(vector<Interval>& intervals) {
sort(intervals.begin(), intervals.end(), [](const Interval& a, const Interval& b) {
return a.start < b.start;
});
priority_queue<Interval, vector<Interval>, greater<Interval>> activeIntervals;
int colors = 0;
for (const auto& interval : intervals) {
while (!activeIntervals.empty() && activeIntervals.top().finish <= interval.start) {
activeIntervals.pop();
}
activeIntervals.push(interval);
colors = max(colors, (int)activeIntervals.size());
}
return colors;
}
int main() {
vector<Interval> intervals = {{1, 3}, {2, 4}, {3, 5}, {1, 5}};
int colors = minColors(intervals);
cout << "Minimum colors needed: " << colors << endl;
return 0;
}
示例输入:
1, 3
2, 4
3, 5
1, 5
示例输出:
Minimum colors needed: 2
问题 5:最小硬币找零问题
问题描述:给定一组硬币面值和一个目标金额,使用最少的硬币数量来凑成目标金额。
解决方案:可以使用贪心算法来解决这个问题。算法从最大的硬币面值开始,逐步选择最大的硬币,直到目标金额为零。
示例代码:
#include <iostream>
#include <vector>
using namespace std;
vector<int> minCoins(vector<int>& coins, int amount) {
vector<int> result;
sort(coins.rbegin(), coins.rend());
for (int i = 0; i < coins.size() && amount > 0; ++i) {
while (amount >= coins[i]) {
amount -= coins[i];
result.push_back(coins[i]);
}
}
if (amount > 0) {
return {}; // 无法凑成目标金额
}
return result;
}
int main() {
vector<int> coins = {25, 10, 5, 1};
int amount = 63;
vector<int> result = minCoins(coins, amount);
if (result.empty()) {
cout << "Cannot make the amount with the given coins." << endl;
} else {
cout << "Minimum coins needed: " << result.size() << endl;
for (int coin : result) {
cout << coin << " ";
}
cout << endl;
}
return 0;
}
示例输入:
25, 10, 5, 1
63
示例输出:
Minimum coins needed: 6
25 25 10 1 1 1
总结
贪心算法通过在每一步选择当前最优解,希望最终得到全局最优解,但并不总是有效。