一、基本概念
贪心算法是一种 “活在当下,只看眼前” 的算法策略。它在每一步都做出当前看起来最好的选择,并期望通过这一系列的局部最优选择,最终导致一个全局最优解。
一个经典比喻:找零钱问题。假设硬币体系为[100, 50, 20, 10, 5, 1],店员需要找给你 136元。贪心策略:每一步都选择面值不超过剩余金额的最大硬币。选100元,剩36元。选20元,剩16元。选10元,剩6元。选5元,剩1元。选1元,完成。最终方案是:100+20+10+5+1,共5枚硬币。这确实是最优解。
核心思想:1)局部最优:每一步都只考虑当前情况下的最佳选择,不考虑整体和未来。
2)不可回溯:一旦做出选择,就不会再回头改变。这和回溯算法(会“撤销”选择)形成鲜明对比。3)高效性:因为不做回溯,通常时间复杂度很低。
二、适用场景
贪心算法不能保证对所有问题都得到全局最优解(比如,如果硬币体系是[4, 3, 1],要找6元,贪心会给出4+1+1,而不是更优的3+3)。因此,判断一个问题能否用贪心算法至关重要。必须同时满足以下两个条件:
1、首先是贪心选择性质,也就是说,每一步的局部最优解能最终导向全局最优解。这通常需要数学证明或逻辑推理。
2、其次是最优子结构,问题的最优解包含了子问题的最优解。解决了大问题,就意味着小问题也已经以最佳方式解决了。
简单总结:如果能证明“每一步都贪心”最终能得到全局最优,并且子问题的解是最优的,那么就可以用贪心算法。
三、典型题目
1)分糖果:先对饼干和小孩都加以排序,遍历小孩最小的小孩获取满足要求的饼干
2)摇摆序列: 设定一个反转状态(start/increase/decrease),每个状态尽可能多包含元素,切换一次摆动增加
3)移掉K位数字 保持结果最小:分析删除规律,每次变小的时删除前比其大的,前面没有数零丢弃,到最后还有未丢弃的从末尾开始丢弃, 可以利用栈来做(单调栈:单调递增栈,每次放进栈的数单向递增,如果发现放入的元素再变小,就把原来的栈顶进行出栈操作)。注意最后栈内容生成结果时要反向;
3)跳跃游戏:每个下标可以达到的最大距离为 i + nums[i], 只需要遍历index过程,刷新最大可到距离 判定可以到nums.size()即可。
4)跳跃游戏2(最少动作):相比跳跃游戏,记录过程最大max_reach和当前reach,当发现reach不够的时候,更新max_reach,并记录一次跳跃
5)用最少数量弓箭引爆气球:先对坐标排序, 每一只箭都尽可能多的射到更多气球,遍历过程超过新气球左边说明也能射中,超过右边界,要回退保证都能射中
6)最优加油法:按节点用记录路过位置的油量,每次经可能往前多走,发现油不够就取记录最大油节点油量 并 统计次数,如果全取完还不够就说明到不了;简化技巧 用大顶堆记录油节点简化统计复杂度,大顶堆:堆顶(根节点)的元素是整个堆中最大的元素。
假设你是一位家长,想要给你的孩子们一些小饼干。每个孩子最多只能给一块饼干。对每个孩子 i ,都有一个胃口值 gi ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j ,都有一个尺寸 sj 。如果 sj >= gi ,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
class Solution {
public:
int findContentChildren(vector<int>& g, vector<int>& s) {
/* 贪心,先对饼干和小孩都加以排序,遍历小孩最小的小孩获取满足要求的饼干 */
int s_index = 0;
int g_index = 0;
sort(g.begin(), g.end());
sort(s.begin(), s.end());
while (s_index < s.size() && g_index < g.size()) {
if (g[g_index] <= s[s_index]) { // 小孩被满足
g_index++;
}
s_index++;
}
return g_index;
}
};
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。
class Solution {
typedef enum {
STATE_START = 0, // 给起始状态的,可能存在连续相同的数值,状态既不属于递增也不属于递减
STATE_INCREASE,
STATE_DECREASE,
} state_mode;
public:
int wiggleMaxLength(vector<int>& nums) {
/* 贪心, 设定一个反转状态(start/increase/decrease),每个状态尽可能多包含元素,切换一次摆动增加,
start是给起始状态的,因为可能存在连续相同的数值,状态既不属于递增也不属于递减 */
if (nums.size() == 0) {
return 0;
}
if (nums.size() == 1) {
return 1;
}
state_mode state = STATE_START;
int result = 1;
for (int i = 1; i < nums.size(); i++) {
// 起始状态
if (state == STATE_START) {
if (nums[i] > nums[i-1]) {
state = STATE_INCREASE;
result++;
} else if (nums[i] < nums[i-1]) {
state = STATE_DECREASE;
result++;
}
// cout << "start " << i << " " << nums[i] << endl;
// 递增
} else if (state == STATE_INCREASE) {
if (nums[i] < nums[i-1]) {
state = STATE_DECREASE;
result++;
}
// cout << "up " << i << " " << nums[i] << endl;
// 递减
} else {
if (nums[i] > nums[i-1]) {
state = STATE_INCREASE;
result++;
}
// cout << "down " << i << " " << nums[i] << endl;
}
}
return result;
}
};
移除K个数字:402. 移掉 K 位数字 - 力扣(LeetCode)
给定一个以字符串表示的非负整数 num,移除这个数中的 k 位数字,使得剩下的数字最小。
class Solution {
public:
string removeKdigits(string num, int k) {
/* 贪心,分析数值删除规律,每次变小的时候删除前比其面大的,如果前面没有数零丢弃,
到最后如果还有未丢弃的,从低位开始丢弃, 可以利用栈来做 */
stack <char> result;
for (int i = 0; i < num.size(); i++) {
// 只要变小,就丢弃比此数大的数,
while (!result.empty() && num[i] < result.top() && k > 0) {
// cout << "pop" << result.top() << endl;
result.pop();
k--;
}
if (!result.empty() || num[i] != '0') { // 为空的时候零丢弃
// cout << "push" << num[i] << endl;
result.push(num[i]);
}
}
// 遍历完成后还有没丢完的,继续出栈
while (k > 0 && !result.empty()) {
// cout << "end_pop" << result.top() << endl;
result.pop();
k--;
}
// 把栈转化为字符串
string ret;
while (!result.empty()) {
ret += result.top();
result.pop();
}
reverse(ret.begin(), ret.end()); // 记得反序,由于出栈后顺序颠倒
return ret.size() == 0 ? "0" : ret; // 结尾需要判定是否都丢完了,要返回0
}
};
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个位置。
class Solution {
public:
bool canJump(vector<int>& nums) {
/* 贪心,每个下标可以达到的最大距离为 i + nums[i],
只需要遍历index过程,判定每个位置是否可达,刷新最大可到距离>=nums.size()算到终点 */
if (nums.size() == 0) {
return false;
}
int max_can_reach = nums[0]; // 初始能力为nums[0]大小, 在位置0
for (int i = 1; i < nums.size(); i++) {
// 判断i是否可以到, 不可到直接放弃
if (max_can_reach < i) {
return false;
}
// 可到的情况判定是否要刷新最大距离
max_can_reach = max(max_can_reach, i + nums[i]);
// 如果已经发现可以到终点了,直接退出,不用再检查了
if (max_can_reach >= nums.size()) {
break;
}
}
return true;
}
};
跳远游戏2:45. 跳跃游戏 II - 力扣(LeetCode)
给定一个非负整数数组,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
class Solution {
public:
int jump(vector<int>& nums) {
/* 贪心,每个下标可以达到的最大距离为 i + nums[i],
只需要遍历index过程,记录过程最大max_reach和当前reach,当发现reach不够
的时候,更新max_reach,并记录一次跳跃 */
int can_reach = 0; // 初始在位置0,一次都没跳
int max_can_reach = 0;
int step = 0;
for (int i = 0; i < nums.size(); i++) {
// 如果当前的能力到不到,选择记录的最大max,多一次跳跃
if (can_reach < i) {
// cout << "update" << max_can_reach << endl;
can_reach = max_can_reach;
step++;
}
// 刷新最大距离
max_can_reach = max(max_can_reach, i + nums[i]);
}
return step;
}
};
射击气球:452. 用最少数量的箭引爆气球 - 力扣(LeetCode)
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。
由于它是水平的,所以y坐标并不重要,因此只要知道开始和结束的x坐标就足够了。
开始坐标总是小于结束坐标。平面内最多存在104个气球。
一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,
若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,
则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
class Solution {
public:
int findMinArrowShots(vector<vector<int>>& points) {
/* 贪心,先对坐标排序, 每一只箭都尽可能多的射到更多气球,
遍历过程超过新气球左边说明也能射中,超过右边界,要回退保证都能射中 */
if (points.size() == 0) {
return 0;
}
sort(points.begin(), points.end()); // 先对坐标排序,默认比较第一元素,左坐标
int num = 1;
int shoot_pos = points[0][1]; // 第一支箭先取最右边的位置
for (int i = 1; i < points.size(); i++) {
if (shoot_pos >= points[i][0]) { // 这只箭超过新气球左边,说明也能射中
shoot_pos = min(points[i][1], shoot_pos); // 如果超过右边界,回退一部分保证都能射中
} else { // 无法射中,就添加射箭数量,更新射箭位置
num++;
shoot_pos = points[i][1];
}
}
return num;
}
};
最优加油法:774. 最小化去加油站的最大距离 - 力扣(LeetCode)
已知一条公路上,有一个起点和一个终点,这之间有n个加油站。
已知:从这n个加油站到终点的距离d与各个加油站可以加油的量L,
起点位置至终点的距离L与起始时刻油箱的油量P。假设1个单位的汽油走1个单位的距离,
油箱没有上限,最少加几次油,可以从起点开至终点?(无法到达返回-1)
//L起点到终点的距离,P起点初始汽油量,pair<加油站到终点距离,加油站汽油量>
int get_mini(int L, int P, vector<pair<int, int> > & stop)
{
priority_queue<int> gas_heap;//存储油量的最大堆
int result = 0;
stop.push_back(make_pair(0, 0)); /* 将终点作为一个停靠点,添加至stop */
sort(stop.begin(), stop.end(), cmp); /* 以停靠点至终点距离从大到小排序 */
for(int i = 0; i < stop.size(); ++i) {
/* 当前要走的距离就是 L - 下一个停靠点至终点的距离 */
int dis = L - stop[i].first;
/* 当前油量不够行驶下一段距离时,就加油 */
while(P < dis && gas_heap.empty() != true) {
P += gas_heap.top();
gas_heap.pop();
result++;
}
/* 距离没走完 油没了 返回失败 */
if(P < dis && gas_heap.empty() == true) {
return -1;
}
P = P - dis; /* 更新剩余汽油 */
L = stop[i].first; /* 更新剩余距离 */
gas_heap.push(stop[i].second); /* 把当前站的汽油放到堆中 备用 */
}
return result;
}