😀大家好,我是白晨,一个不是很能熬夜😫,但是也想日更的人✈。如果喜欢这篇文章,点个赞👍,关注一下👀白晨吧!你的支持就是我最大的动力!💪💪💪
文章目录
🍊前言
观前提示:此文章需要一定贪心算法的基础。
大家好呀,我是白晨🧐。
贪心算法算是一种比较耳熟能详的算法,只要求出局部最优解就可以得到整体的最优解,而且面试很喜欢出这种问题。但是,贪心算法其实并不好想,特别是有些问题比较绕的时候,你可能根本就想不到贪心算法。动态规划这个算法是从整体出发求整体最优,而贪心算法是从局部出发求整体最优,所以很多时候贪心算法并不好用,而且动态规划说是难,但是掌握方法后使用起来非常爽,而贪心算法规律比较难找,每一种题都有不同的具体贪心思想。
所以,白晨选出了一些贪心算法的几道经典题目,以帮助大家理解贪心算法。题目难度由低到高,都是比较经典的题型。可能不是很难,但是有些题目很绕,看了答案后,会觉得自己被侮辱了,这就是贪心算法🤪。
🍋贪心算法经典题目
🌸1.分割平衡字符串
原题链接:分割平衡字符串
解析:
这道题要求尽可能多的切割平衡字符串
- 我们通过观察例题以及推理可得,要想得到最多切割,切割处来的字符串必须为先
R
后L
或者先L
后R
。绝对不能出现L
R
相互交错出现。 - 也就是说分割时,不能出现嵌套。
- 贪心策略: 不要有嵌套的平衡,只要达到平衡,就立即分割
- 故可以定义一个变量
cnt
,在遇到不同的字符时,向不同的方向变化,设cnt
为 0 时达到平衡,分割数更新。 - 比如:
从左往右扫描字符串s,遇到L
, cnt + 1,遇到R
,cnt - 1
当cnt
为0时即,更新记录平衡字符串子串的变量++num
class Solution {
public:
int balancedStringSplit(string s) {
int len = s.size();
int cnt = 0, num = 0;
for (int i = 0; i < len; ++i)
{
// 遇到R,--cnt
if (s[i] == 'R')
--cnt;
// 遇到L,++cnt
else
++cnt;
// 判断:如果cnt等于0,说明要切割
if (cnt == 0)
++num;
}
return num;
}
};
🌹2.买卖股票的最佳时机
原题链接:买卖股票的最佳时机
解析:
这道题非常具有迷惑性,也特别容易想复杂,当你看到答案的时候可能会想找个豆腐撞死。
-
法1:贪心算法
其实你完全没有必要去想怎么买卖才能利益最大化,因为这个题目是给出了后面几天的股价的,所以我们可以到第二天再做决策。
所以,第二天股价涨时,把我们就买入前一天的股票,再前一天的股票卖出,也就是第二天得到了利润
如果第二天跌了,我们第一天就不买入
总结一下就是:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int day = prices.size();
int profit = 0;
for (int i = 1; i < day; ++i)
{
// 股价比前一天高就卖出
if (prices[i] > prices[i - 1])
profit += prices[i] - prices[i - 1];
}
return profit;
}
};
-
法二:动态规划
这道题也能用动态规划做,就是很麻烦,而且效果没有贪心好。如果没有学过动态规划的同学可以不用看这种方法。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int day = prices.size();
vector<vector<int> > ret(day, vector<int>(2, 0));
ret[0][0] = 0;
ret[0][1] = -prices[0];
for (int i = 1; i < day; ++i)
{
ret[i][0] = max(ret[i - 1][0], ret[i - 1][1] + prices[i]);
ret[i][1] = max(ret[i - 1][1], ret[i - 1][0] - prices[i]);
}
return ret[day - 1][0];
}
};
优化算法:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int day = prices.size();
int ret[2];
ret[0] = 0;
ret[1] = -prices[0];
int tmp0 = ret[0];
int tmp1 = ret[1];
for (int i = 1; i < day; ++i)
{
ret[0] = max(tmp0, tmp1 + prices[i]);
ret[1] = max(tmp1, tmp0 - prices[i]);
tmp0 = ret[0];
tmp1 = ret[1];
}
return ret[0];
}
};
🌺3.跳跃游戏
原题链接:跳跃游戏
解析:
class Solution {
public:
bool canJump(vector<int>& nums) {
int maxLen = 0;
int len = nums.size();
for(int i = 0; i < len; ++i)
{
// 必须要判断前面最大能不能到达这个点
// 能到达才更新,不能到达就不更新
if(i <= maxLen)
maxLen = max(i + nums[i], maxLen);
// 遇到最大到达距离大于等于最后一个数的下标就返回true
if(maxLen >= len - 1)
return true;
}
return false;
}
};
// 改版
class Solution {
public:
bool canJump(vector<int>& nums) {
int maxLen = 0;
int len = nums.size();
for(int i = 0; i < len; ++i)
{
// 通过上面的代码,相信很多人已经发现,如果到最大到达距离达不了遍历中的某一个元素
// 那么,最后一定到不了最后一个元素
// 因为最大到达距离是覆盖式的,能到最后一个元素必然可以到达前面的元素
// 所以我们可以先判断是否能到这个元素,能到达就更新,不能到达就直接返回false
if(maxLen < i)
return false;
maxLen = max(i + nums[i], maxLen);
}
// 遍历完成说明必然可以到达最后一个元素,直接返回true
return true;
}
};
🌻4.多机调度问题
贪心思路:
bool cmp(const int& x, const int& y)
{
return x > y;
}
int greedStrategy(const vector<int>& works, vector<int>& machines)
{
// 按工作时间长短排序
sort(works.begin(), works.end(), cmp);
int workNum = works.size();
int machineNum = machines.size();
// 如果 n <= m ,返回任务中时间最长的一项任务
if (workNum <= machineNum)
return works[0];
// 选择机器执行每一个任务
for (int i = 0; i < workNum; ++i)
{
int flag = 0;
// 选择现在执行任务花费时间最少的机器
for (int j = 1; j < machineNum; ++j)
{
if (machines[j] < machines[flag])
flag = j;
}
// 执行本次任务
machines[flag] += works[i];
}
// 选出用时最长的机器
int max = 0;
for(int e : machines)
{
if (e > max)
max = e;
}
// 返回时间
return max;
}
🌼5.活动选择
- 每次选取结束时间最早的活动,可以得到最优解。按这种方法选择,可以为未安排活动留下尽可能多的时间。如上图所示的活动集合S,其中各项活动按照结束时间单调递增排序
面对活动选择会遇到以下三种情况:
- 开始时间和结束时间都不同
这里我们选择结束时间早的。
- 开始时间相同,结束时间不同
这里我们选择结束时间早的
- 开始时间不同,结束时间相同
- 这里我们先要判断是否两个活动都在上个被举行活动结束之后
- 两个都在之后,选择遍历先遇到的即可
- 一个在之前,一个在之后,选择之后的
- 两个都没有在之后,不选
- 当然,这里的两个只是例子,实际情况可能有多个结束时间相同的活动,判断逻辑与上面相同。
bool cmp(const pair<int, int>& a, const pair<int, int>& b)
{
return a.second < b.second;
}
int greedyActivitySelector(const vector<pair<int, int>>& act)
{
// 按照结束时间从早到晚排序
sort(act.begin(), act.end(), cmp);
// 记录一个活动的结束时间
int end = 1;
int num = 0;
// 遍历每一个活动
for (auto e : act)
{
// 当一个活动的开始时间大于等于前一个被举行活动的结束时间时,执行这个活动
if (e.first >= end)
{
end = e.second;
++num;
}
}
return num;
}
🌷6.最多可以参加的会议数目
原题链接:最多可以参加的会议数目
这个题与上题类似,我们可以使用贪心算法,按照结束时间先后去参加,先结束的先参加。
但是这道题上道题不同的地方在于,会议有持续时间,而且可以只选择一天去,所以多个会议持续时间重叠并可能不影响参加会议,这道题的难度更大。
法一:贪心算法硬解
思路详解:
直接莽夫,记录下已经参加会议过的天。每次选择结束最早的会议,遍历这个会议的开始日期到结束日期,判断是否有未参加会议的天,如果有,就参加,如果没有,就不参加。
class Solution {
public:
int maxEvents(vector<vector<int>>& events) {
// 按照结束顺序排升序,如果结束时间相同,就将开始时间早的排在前
sort(events.begin(), events.end(), [](vector<int>& a, vector<int>& b) {
if (a[1] != b[1]) return a[1] < b[1];
return a[0] < b[0];
});
// 记录参会次数
int num = 0;
// 创建一个数组,判断这天有没有使用
vector<bool> used(100001, false);
for (auto e : events)
{
// 遍历一个会议的开始到结束,判断是否有空闲的天
for (int i = e[0]; i <= e[1]; ++i)
{
if (!used[i])
{
used[i] = true;
++num;
break;
}
}
}
return num;
}
};
这种方法可以说简单粗暴,但是相应的,时间复杂度取决于题目给的数据,如果给出一组例如
[
1
,
1
]
,
[
1
,
2
]
,
[
1
,
3
]
,
[
1
,
4
]
.
.
.
.
.
.
.
[
1
,
10000
]
[1,1],[1,2],[1,3],[1,4].......[1,10000]
[1,1],[1,2],[1,3],[1,4].......[1,10000]
开始时间相同,但是结束时间不同的数据,每一次都要从开始到结束遍历,时间复杂度非常之高,会导致时间太长而无法通过。
所以,直接莽行不太通。
法二:队列+贪心
思路解析:
- 这次我们按照开始顺序进行排序,排成升序。
- 然后,从第一天开始走,遇到这天开始的会议按照结束时间加入小根堆,这样可以保证,结束时间早的会议先被使用。
- 其次,由于一天只能参加一个会议,所以有些会议可能参加不了,如 [[1,4],[4,4],[2,2],[3,4],[1,1]] ,里面的 [ 4 , 4 ] [4,4] [4,4] 就因为前面的会议太多,第四天参加不了,第五天会议已经结束而被迫放弃这个会议。所以,我们每天要放弃掉已经过时的会议。
- 接下来,选出小根堆的堆顶,也就是选择结束时间最早的那一个会议参加。
- 重复 2~4 步,直到堆为空。
class Solution {
public:
int maxEvents(vector<vector<int>>& events) {
priority_queue<int, vector<int>, greater<int>> pq;
sort(events.begin(), events.end());
int curDay = 1;
int i = 0;
int n = events.size();
int ans = 0;
while (i < n || !pq.empty()) {
// 队列存储已经开始的会议, 按结束时间小根堆排序
while (i < n && events[i][0] == curDay) {
pq.push(events[i][1]);
i++;
}
// 已经结束的会议出堆
while (!pq.empty() && pq.top() < curDay) {
pq.pop();
}
// 贪心思想, 选择最早结束的会议参加
if (!pq.empty()) {
pq.pop();
ans++;
}
curDay++;
}
return ans;
}
};
🌱7.无重叠区间
原题链接:无重叠区间
法一:
要求最小移除的区间数,也就是求最大不重叠的区间,所以我们可以按照活动选择的方法,先求出最大不重叠区间数,再用总区间数减去最大不重叠区间数,就可以得到最小移除的区间数。
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b)
{
return a[1] < b[1];
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if (intervals.size() == 0)
return 0;
// 按结束时间从早到晚排序
sort(intervals.begin(), intervals.end(), cmp);
// 由于区间可能为负,并且第一个结束的区间一定是我们选择的不重叠的,所以我们这里先记录第一个区间
// 记录不重叠区间的数量
int num = 1;
// 记录上一个不重叠区间的结束
int end = intervals[0][1];
for (int i = 1; i < intervals.size(); ++i)
{
// 如果上一个不重叠区间的结束小于等于这个区间的开始,这个区间为不重叠区间
if (end <= intervals[i][0])
{
end = intervals[i][1];
++num;
}
}
return intervals.size() - num;
}
};
当然,这个题也可以用范围for
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b)
{
return a[1] < b[1];
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if (intervals.size() == 0)
return 0;
sort(intervals.begin(), intervals.end(), cmp);
int num = 0;
// 把end设置为int类型的最小值
int end = INT_MIN;
for (auto e : intervals)
{
if (end <= e[0])
{
end = e[1];
++num;
}
}
return intervals.size() - num;
}
};
这种方法时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn) ,可以说已经是这个题最简单的算法。
法二:
根据题目求解:
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b)
{
//按起点递增排序
if (a[0] != b[0])
return a[0] < b[0];
return a[1] < b[1];
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if (intervals.empty())
return 0;
// 排序
sort(intervals.begin(), intervals.end(), cmp);
// 删除的个数
int Dnum = 0;
// 保存前一个不重叠区间的下标
int prev = 0;
for (int i = 1; i < intervals.size(); ++i)
{
// 当区间重叠时
if (intervals[prev][1] > intervals[i][0])
{
// 判断是否为情况2
if (intervals[prev][1] > intervals[i][1])
{
prev = i;
}
++Dnum;
}
//情况3
else
{
prev = i;
}
}
return Dnum;
}
};
🍍总结
我们简单总结一下贪心算法的解题过程:
- 建立数学模型来描述问题;
- 把求解的问题分成若干个子问题;
- 对每一子问题求解,得到子问题的局部最优解;
- 把子问题的局部最优解合成原来解问题的一个解
这是一个新的系列 ——【刷题日记】,白晨开这个系列的初衷是为了分享一些不同算法经典题型,以便于大家更好的学习编程。如果大家喜欢这个专栏的话——点击查看【刷题日记】所有文章。
如果解析有不对之处还请指正,我会尽快修改,多谢大家的包容。
如果大家喜欢这个系列,还请大家多多支持啦😋!
如果这篇文章有帮到你,还请给我一个大拇指
👍和小星星
⭐️支持一下白晨吧!喜欢白晨【刷题日记】系列的话,不如关注
👀白晨,以便看到最新更新哟!!!
我是不太能熬夜的白晨,我们下篇文章见。