一、概念
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,所做出的是在某种意义上的局部最优解
贪心选择是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素
当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。运用贪心策略在每一次转化时都取得了最优解。问题的最优子结构性质是该问题可用贪心算法求解的关键特征。贪心算法的每一次操作都对结果产生直接影响。贪心算法对每个子问题的解决方案都做出选择,不能回退
贪心算法的基本思路是从问题的某一个初始解出发一步一步地进行,根据某个优化测度,每一步都要确保能获得局部最优解。每一步只考虑一个数据,其选取应该满足局部优化的条件。若下一个数据和部分最优解连在一起不再是可行解时,就不把该数据添加到部分解中,直到把所有数据枚举完,或者不能再添加算法停止
实际上,贪心算法适用的情况很少。一般对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可以做出判断
贪心算法存在的问题:
不能保证求得的最后解是最佳的
不能用来求最大值或最小值的问题
只能求满足某些约束条件的可行解的范围
二、选择排序
难度:Easy
选择排序采用的即为贪心策略。其所采用的贪心策略即为每次从未排序的数据中选取最小值,并把最小值放在未排序数据的起始位置,直到未排序的数据为0,则结束排序
#include <iostream> void Swap(int* num1, int* num2) { if (num1 == num2) return; *num1 += (*num2);//a = a+b *num2 = (*num1) - (*num2);//b = a *num1 -= (*num2); } void SelectSort(int* arr, int size) { for (int i = 0; i < size - 1; ++i) { int minIndex = i; for (int j = i + 1; j < size; ++j) { if (arr[j] < arr[minIndex]) minIndex = j; } Swap(&arr[i], &arr[minIndex]); } } int main() { int arr[] = { 10,8,6,20,9,77,32,91 }; SelectSort(arr, sizeof(arr) / sizeof(int)); for (int i = 0; i < sizeof(arr)/sizeof(int); ++i) { std::cout << arr[i] << " "; } return 0; }
三、平衡字符串
难度:Easy
贪心策略: 不要有嵌套的平衡,只要达到平衡,立即分割
故可以定义一个变量balance,在遇到不同的字符时,向不同的方向变化,当balance为0时达到平衡时,分割数更新
比如:
从左往右扫描字符串s,遇到L时balance - 1,遇到R时balance + 1。当balance为0时即,更新记录++count,若最后count == 0,说明s只需要保持原样,返回1
class Solution { public: int balancedStringSplit(string s) { int count = 0; int balance = 0; for (int i = 0; i < s.size(); ++i) { if (s[i] == 'L') --balance; if (s[i] == 'R') ++balance; if (balance == 0) ++count; } return count; } };
四、买卖股票的最佳时机
难度:Medium
连续上涨交易日: 第一天买最后一天卖收益最大,等价于每天都买卖
连续下降交易日: 不买卖收益最大,即不会亏钱。
故可以遍历整个股票交易日价格列表,在所有上涨交易日都买卖(赚到所有利润),所有下降交易日都不买卖(永不亏钱)
class Solution { public: int maxProfit(vector<int>& prices) { int ret = 0; for(int i = 1; i < prices.size(); ++i) { int profit = prices[i] - prices[i - 1]; if(profit > 0) ret += profit; } return ret; } };
五、跳跃游戏
难度:Medium
对于数组中的任意一个位置 y,我们如何判断它是否可以到达?根据题目的描述,只要存在一个位置 x,它本身可以到达,并且它跳跃的最大长度为 x + nums[x],这个值大于等于 y,即 x+nums[x] ≥ y,那么位置y也可以到达
依次遍历数组中的每一个位置,并实时维护最远可以到达的位置。对于当前遍历到的位置x,若其在最远可以到达的位置的范围内,那么就可以从起点通过若干次跳跃到达该位置,因此可以用x+nums[x]更新最远可以到达的位置
在遍历的过程中,若最远可以到达的位置大于等于数组中的最后一个位置,那就说明最后一个位置可达,就可以直接返回True。若在遍历结束后,最后一个位置仍不可达,就返回False
class Solution { public: bool canJump(vector<int>& nums) { int maxIndex = 0; for(int i = 0; i < nums.size(); ++i) { if(i <= maxIndex) //i位置在最大范围内,说明i位置可以到达 { maxIndex = max(maxIndex, i + nums[i]);//更新最大范围 if(maxIndex >= nums.size() - 1) return true;//若最大范围包括最后一个位置,说明最后一个位置可以到达 } } return false; } };
六、钱币找零
难度:Easy
运用贪心算法的思想,每一步尽可能用面值大的纸币即可。在日常生活中也是这么做的。在程序中已经事先将Value按照从小到大的顺序排好
#include <iostream> #include <vector> using namespace std; int Solve(int money, vector<pair<int, int>>& moneyCount) { int num = 0; for (int i = moneyCount.size() - 1; i >= 0; --i)//逆序:先使用面值大的钱币 { int count = min(moneyCount[i].second, money / moneyCount[i].first);//需要的数量和拥有的数量中选择较小的 money -= moneyCount[i].first * count; num += count; } if (money != 0) return -1;//找不开钱 return num; } int main() { //first:面值 second:数量 vector<pair<int, int>> moneyCount = { {1,3},{2,1},{5,4},{10,3},{20,0},{50,1},{100,10} }; int money = 0; cout << "请输入需要支付多少钱" << endl; cin >> money; int ret = Solve(money, moneyCount); if (ret == -1) cout << "No" << endl; else cout << ret << endl; return 0; }
七、多机调度问题
难度:Medium
这个问题还没有有效的解法(求最优解),但可以用贪心选择策略设计出较好的近似算法(求次优解)
当n<=m时,只要将作业分给每一个机器即可;当n>m时,先将n个作业从大到小排序,然后依此顺序将作业分配给空闲的机器。即从剩下的作业中,选择需要处理时间最长的,然后依次选择处理时间次长的,直到所有的作业全部处理完毕,或者机器不能再处理其他作业为止。若每次是将需要处理时间最短的作业分配给空闲的机器,那么可能就会出现其它所有作业都处理完了只剩所需时间最长的作业在处理的情况,这样势必效率较低
#include <iostream> #include <vector> #include <algorithm> using namespace std; class Compare { public: bool operator()(int x, int y) { return x > y; } }; int GreedStrategy(vector<int>& works, vector<int>& machines) { //按作业时间从大到小排序 sort(works.begin(), works.end(), Compare()); //若作业数小于等于机器数,直接返回最大的作业时间即可 if (works.size() <= machines.size()) return works[0]; //从大到小为每个作业分配机器 for (int i = 0; i < works.size(); ++i) { //假设选择第一个机器 int minMachines = 0; int time = machines[minMachines]; //从机器中选择作业时间最小的 for (int j = 1; j < machines.size(); ++j) { if (time > machines[j]) { minMachines = j; time = machines[j]; } } //将当前作业交给作业时间最小的机器 machines[minMachines] += works[i]; } //从所有机器中选择总共作业时间最长的 int ret = machines[0]; for (int i = 1; i < machines.size(); ++i) { ret = max(ret, machines[i]); } return ret; } int main() { int n = 0, m = 0; cout << "请输入作业数和机器数" << endl; cin >> n >> m; vector<int> works(n); vector<int> machines(m, 0);//存储每台机器总共的作业时间 cout << "请输入各个作业所需要的时间" << endl; for (int i = 0; i < n; ++i) { cin >> works[i]; } cout << GreedStrategy(works, machines) << endl; return 0; }
八、活动选择
难度:Medium
贪心策略:
每次都选择开始时间最早的活动,不能得到最优解
每次都选择持续时间最短的活动,不能得到最优解
每次选取结束时间最早的活动,可以得到最优解。按这种方法选择,可以为未安排活动留下尽可能多的时间。如下图所示的活动集合S,其中各项活动按照结束时间单调递增排序
#include <iostream> #include <vector> #include <algorithm> using namespace std; class Compare { public: bool operator()(pair<int,int> p1, pair<int,int> p2) { return p1.second < p2.second; } }; int GreedyActivitySelector(const vector<pair<int, int>>& act) { int num = 1;//记录可以举行的活动的个数 int i = 0; for (int j = 1; j < act.size(); ++j) { if (act[j].first >= act[i].second) { i = j; ++num; } } return num; } int main() { int number = 0; cin >> number; vector<pair<int, int>> act(number); for (int i = 0; i < act.size(); ++i) { cin >> act[i].first >> act[i].second; } //按照活动截止日期从小到大排序 sort(act.begin(), act.end(), Compare()); int ret = GreedyActivitySelector(act); cout << ret << endl; return 0; }
九、无重叠区间
难度:Medium
方法一
这一题与第八题《活动选择》十分类似,找出不冲突区间数的最大值,再用区间的总数减去最大值,即可得到需移除区间的最小数量
class Solution { public: static bool cmp(vector<int>& a, vector<int>& b) { return a[1] < b[1]; } int eraseOverlapIntervals(vector<vector<int>>& intervals) { std::sort(intervals.begin(), intervals.end(), cmp); int num = 1; int i = 0; for (int j = 1; j < intervals.size(); ++j) { if (intervals[j][0] >= intervals[i][1]) { i = j; ++num; } } return intervals.size() - num; } };
方法二
按照起点对区间进行排序,贪心算法就可以起到很好的效果
贪心策略:
当按照起点先后顺序考虑区间的时候。利用一个 prev 指针追踪刚刚添加到最终列表中的区间
情况一:
当前考虑的两个区间不重叠:
在这种情况下,不移除任何区间,将 prev 赋值为后面的区间,移除区间数量不变
情况二:
两个区间重叠,后一个区间的终点在前一个区间的终点之前。
这种情况下,可以简单地只用后一个区间。因为后一个区间的长度更小,可以留下更多的空间,容纳更多的区间。因此, prev 更新为当前区间,移除区间的数量 + 1
情况三:
两个区间重叠,后一个区间的终点在前一个区间的终点之后
这种情况下,我们用贪心策略处理问题,直接移除后一个区间。这样也可以留下更多的空间,容纳更多的区间。因此,prev不变,移除区间的数量 + 1
bool cmp(const vector<int>& a, const vector<int>& b) { //按起点递增排序 return a[0] < b[0]; } class Solution { public: int eraseOverlapIntervals(vector<vector<int>>& intervals) { if (intervals.size() == 0) { return 0; } //按起点递增排序 sort(intervals.begin(), intervals.end(), cmp); int end = intervals[0][0], prev = 0, count = 0; for (int i = 1; i < intervals.size(); i++) { if (intervals[prev][1] > intervals[i][0]) {//两个区间冲突 if (intervals[prev][1] > intervals[i][1]) { //情况2 prev = i; } //情况3 count++; } else { //情况1 prev = i; } } return count; } };