专题九 贪心算法
一、无重叠区间
1.题目
Leetcode:第 435 题
给定一个区间的集合 intervals
,其中 intervals[i] = [starti, endi]
。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
示例 1:
输入: intervals = [[1,2],[2,3],[3,4],[1,3]] 输出: 1 解释: 移除 [1,3] 后,剩下的区间没有重叠。
示例 2:
输入: intervals = [ [1,2], [1,2], [1,2] ] 输出: 2 解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3:
输入: intervals = [ [1,2], [2,3] ] 输出: 0 解释: 你不需要移除任何区间,因为它们已经是无重叠的了。
2.解题思路
使用贪心算法解决无重叠区间问题。
在 eraseOverlapIntervals
函数中,首先检查 intervals
是否为空,如果为空,直接返回 0。然后,我们使用 sort
函数和一个自定义的比较函数 cmp
对 intervals
进行排序,确保所有区间按照右边界的升序排列。接下来,我们初始化 count
为 1,因为至少有一个区间不会被删除。之后,我们遍历 intervals
,通过比较当前区间的左边界和上一个区间的结束点 end
来判断两个区间是否重叠。如果不重叠,我们就更新 end
并增加 count
。最终,函数返回 intervals
的总数减去 count
,即得到需要删除的重叠区间的数量。
3.实现代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
// cmp 函数用于比较两个 vector<int> 对象,即比较两个区间的右边界
// 这个函数被用作 sort 函数的比较函数
static bool cmp(const vector<int>& a, const vector<int>& b) {
// 返回 a 的右边界是否小于 b 的右边界
return a[1] < b[1];
}
// eraseOverlapIntervals 函数用于找出给定区间中重叠区间的最小数量
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
// 如果 intervals 为空,没有重叠区间,返回 0
if (intervals.size() == 0) return 0;
// 对 intervals 进行排序,按照区间的右边界进行升序排序
sort(intervals.begin(), intervals.end(), cmp);
// 初始化计数器 count 为 1,因为至少有一个区间
int count = 1;
// 初始化 end 为第一个区间的右边界,作为当前考虑的区间分割点
int end = intervals[0][1];
// 遍历 intervals,从第二个区间开始(索引为 1)
for (int i = 1; i < intervals.size(); i++) {
// 如果当前区间的左边界大于或等于上一个区间的分割点 end,
// 则当前区间与上一个区间不重叠,可以保留
if (end <= intervals[i][0]) {
// 更新分割点为当前区间的右边界
end = intervals[i][1];
// 增加非重叠区间的计数
count++;
}
// 如果当前区间与上一个区间重叠,则忽略当前区间(不需要增加 count)
}
// 返回 intervals 的总数减去非重叠区间的数量,即为需要删除的重叠区间数量
return intervals.size() - count;
}
};
//测试
int main()
{
Solution p;
vector<vector<int>> intervals = { {1, 2}, {2, 3}, {3, 4}, {1, 3} };
int result = p.eraseOverlapIntervals(intervals);
cout << "需要移除区间的最小数量:" << result << endl;
cout << endl;
return 0;
}
二、划分字母区间
1.题目
Leetcode:第 763 题
给你一个字符串 s
。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s
。
返回一个表示每个字符串片段的长度的列表。
示例 1:
输入:s = "ababcbacadefegdehijhklij" 输出:[9,7,8] 解释: 划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。 每个字母最多出现在一个片段中。 像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。
示例 2:
输入:s = "eccbbbbdec" 输出:[10]
2.解题思路
使用贪心算法解决划分字母区间问题。
在 partitionLabels
函数中,首先使用一个 hash
数组来记录每个字符在字符串 S
中最后出现的位置。然后,我们使用两个指针 left
和 right
来确定每个子字符串的范围。left
指向子字符串的开始,而 right
指向子字符串的结束。我们两次遍历字符串 S
:第一次是为了填充 hash
数组,第二次是为了找到每个子字符串的范围。在第二次遍历中,我们使用 max
函数来更新 right
的值,确保它始终指向当前子字符串中最远的字符位置。每当我们遇到一个字符,其索引等于当前 right
的值时,我们就知道找到了一个子字符串的结束位置。然后,我们计算这个子字符串的长度,并将其添加到结果数组 result
中。最后,我们更新 left
的值,以便开始寻找下一个子字符串。这种方法可以确保每个子字符串都是由连续的字符组成的,且这些字符在原始字符串中出现的顺序与它们在子字符串中的顺序相同。
3.实现代码
class Solution {
public:
// partitionLabels 函数用于将字符串 S 分割成由连续字母组成的独立子字符串
vector<int> partitionLabels(string S) {
int hash[27] = { 0 }; // hash数组,用于记录每个字符最后出现的位置(a-z)
// 初始化所有字符的位置为0,字符'a'对应的下标是0,字符'z'对应的下标是25
// 遍历字符串 S,统计每个字符最后出现的位置
for (int i = 0; i < S.size(); i++) {
// 将字符转换为下标('a'到'z'对应的下标是0到25)
hash[S[i] - 'a'] = i;
// 更新字符在字符串中最后出现的位置
}
vector<int> result; // 用于存储分割后子字符串的长度
int left = 0; // 记录当前子字符串的起始位置
int right = 0; // 记录当前子字符串的结束位置
// 再次遍历字符串 S
for (int i = 0; i < S.size(); i++) {
// 更新 right 为当前字符和之前所有字符中最远的位置
right = max(right, hash[S[i] - 'a']);
// 如果当前字符的位置等于 right,则说明已经到达了当前子字符串的结束位置
if (i == right) {
// 计算当前子字符串的长度,并添加到结果中
result.push_back(right - left + 1);
// 更新下一个子字符串的起始位置
left = i + 1;
}
}
// 返回分割后子字符串的长度数组
return result;
}
};
//测试
int main()
{
Solution p;
string s = "ababcbacadefegdehijhklij";
vector<int> result = p.partitionLabels(s);
cout << "分割后子字符串的长度数组:" << endl;
cout << "[";
for (auto& i : result) {
cout << i << ",";
}
cout << "]"<<endl;
cout << endl;
return 0;
}
三、合并区间
1.题目
Leetcode:第 56 题
以数组 intervals
表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]
。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]] 输出:[[1,6],[8,10],[15,18]] 解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals = [[1,4],[4,5]] 输出:[[1,5]] 解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
2.解题思路
使用贪心算法解决合并区间问题。
在 merge
函数中,首先检查 intervals
是否为空,如果为空,直接返回空的结果集 result
。然后,我们使用 sort
函数和一个 lambda 表达式对 intervals
进行排序,确保所有区间按照它们的左边界进行升序排列。接下来,我们将第一个排序后的区间添加到结果集 result
中。之后,我们遍历排序后的区间集合,从第二个区间开始。对于每个区间,我们检查它是否与结果集中的最后一个区间重叠。如果重叠(即当前区间的左边界大于或等于结果集中最后一个区间的右边界),我们更新结果集中最后一个区间的右边界为两个区间右边界的最大值。如果不重叠,我们将当前区间添加到结果集中。最终,我们返回合并后的区间集合 result
。这个方法利用了排序来简化重叠区间的检测和合并过程。
3.实现代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
// merge 函数用于合并区间
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> result;
// 如果区间集合为空,直接返回空的结果集
if (intervals.size() == 0) return result;
// 使用 lambda 表达式作为 sort 函数的比较函数,按照区间的左边界进行升序排序
sort(intervals.begin(), intervals.end(), [](const vector<int>& a, const vector<int>& b) {
return a[0] < b[0];
});
// 将排序后的区间集合中的第一个区间放入结果集中
// 因为结果集中的区间不能有重叠,所以先放入第一个区间作为起始点
result.push_back(intervals[0]);
// 从区间集合的第二个区间开始遍历
for (int i = 1; i < intervals.size(); i++) {
// 如果当前区间的左边界大于结果集中最后一个区间的右边界,说明没有重叠
if (result.back()[1] >= intervals[i][0]) {
// 发现重叠区间,更新结果集中最后一个区间的右边界为两个区间右边界的最大值
// 因为 result.back() 的左边界一定是当前结果集中所有区间中最小的,所以不需要更新
result.back()[1] = max(result.back()[1], intervals[i][1]);
}
else {
// 当前区间与结果集中的最后一个区间不重叠,将其添加到结果集中
result.push_back(intervals[i]);
}
}
// 返回合并后的区间集合
return result;
}
};
//测试
int main()
{
Solution p;
vector<vector<int>>intervals = {{1, 3}, {2, 6}, {8, 10}, {15, 18}};
vector<vector<int>> result = p.merge(intervals);
cout << "合并后的区间集合:" << endl;
for (auto& ans : result) {
cout << "[";
for (auto& i : ans) {
cout << i << ",";
}
cout << "]" << " ,";
}
cout << endl;
cout << endl;
return 0;
}
ps:以上皆是本人在探索算法旅途中的浅薄见解,诚挚地希望得到各位的宝贵意见与悉心指导,若有不足或谬误之处,还请多多指教。