第八章 贪心算法 part04
今天的三道题目,都算是 重叠区间 问题,大家可以好好感受一下。都属于那种看起来很复杂,但一看贪心解法,惊呼:这么巧妙!
这种题还是属于那种,做过了也就会了,没做过就很难想出来。
不过大家把如下三题做了之后,重叠区间 基本上差不多了。
452. 用最少数量的箭引爆气球
class Solution {
public:
static bool cmp(const vector<int>&a,const vector<int>&b){
return a[1] < b[1];
}
int findMinArrowShots(vector<vector<int>>& points) {
sort(points.begin(),points.end(),cmp);
int right=points[0][1];
int arrow=1;
for(int i=1;i<points.size();i++)
{
if(points[i][0]>right)
{
arrow++;
right=points[i][1];
}
}
return arrow;
}
};
直接把这张图看一遍即可,能看懂基本上就没什么问题了.
最开始看完思路, 我写的代码排序是按左边界排序的,左边相同按右边界排序。然而,正确的贪心策略应该是先排序所有气球的右边界,然后判断下一个气球是否能够被当前的箭头射中。因为,如果按照我的思路,如果按照我的思路,只需要一支箭,实际上,需要四支箭
[1, 10], [2, 3], [4, 5], [6, 7], [8, 9]
所以应该按照右边界排序
[[2, 3], [4, 5], [6, 7], [8, 9], [1, 10]]
按右边界排序确保你每次都选择最小的右边界,即射中当前气球组中结束最早的气球。
435. 无重叠区间
class Solution {
public:
static bool cmp(const vector<int>&a,const vector<int>&b){
if( a[0] == b[0])
return a[1] > b[1];
return a[0] < b[0];
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
sort(intervals.begin(),intervals.end(),cmp);
int num=0;
int start=intervals[0][0];
int end=intervals[0][1];
for(int i=1;i<intervals.size();i++)
{
if(intervals[i][0]>=start&&intervals[i][1]<=end)
num++;
else if(intervals[i][0]<=end&&intervals[i][1]>end)
end=intervals[i][1];
else if(intervals[i][0]>end)
{
start=intervals[i][0];
end=intervals[i][1];
}
}
return num;
}
};
最开始写的代码如上面所示,结果发现,这不是最小,反而是最大的删除区间.比如下面的数组,删除[1,2],[2,3], 实际上只要删除[1,3]. 这个代码就是尽可能扩展范围,在范围内的统统删掉.确实
[[1,2],[2,3],[3,4],[1,3]].这题最大的难度,还是怎么确保最少,就很头疼.
转换思路,最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了。所以要转换思路.最开始按照左边界排序,按左边界排序在很多情况下能工作,但按右边界排序是一种更好的贪心策略,因为这能够确保选择右边界最早结束的区间,从而避免不必要的重叠,进一步减少删除区间的数量。
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) {
sort(intervals.begin(),intervals.end(),cmp);
int num=1;
int end=intervals[0][1];
for(int i=1;i<intervals.size();i++)
{
if(intervals[i][0]>=end)
{
num++;
end=intervals[i][1];
}
}
return intervals.size()-num;
}
};
真正的按左边界如下所示:最重要的还是这句语法min(end, intervals[i][1]);选择最小范围.
class Solution {
public:
static bool cmp (const vector<int>& a, const vector<int>& b) {
return a[0] < b[0]; // 改为左边界排序
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if (intervals.size() == 0) return 0;
sort(intervals.begin(), intervals.end(), cmp);
int count = 0; // 注意这里从0开始,因为是记录重叠区间
int end = intervals[0][1]; // 记录区间分割点
for (int i = 1; i < intervals.size(); i++) {
if (intervals[i][0] >= end) end = intervals[i][1]; // 无重叠的情况
else { // 重叠情况
end = min(end, intervals[i][1]);
count++;
}
}
return count;
}
};
763. 划分字母区间
读题目都脑筋急转弯了一下,就是同一个字母在同一个字符串中,最多可以划分为多少层. 这么一思考,发现下面的思路确实很清晰. 遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了
统计每一个字符最后出现的位置, 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点
class Solution {
public:
vector<int> partitionLabels(string s) {
vector<int> hash(26,0);
for(int i=0;i<s.size();i++)
{
hash[s[i]-'a']=i;
}
vector<int> result;
int left = 0;
int right = 0;
int length=0;
for(int i=0;i<s.size();i++)
{
length++;
right = max(right, hash[s[i] - 'a']);
if(i==right)
{
result.push_back(length);
length=0;
}
}
return result;
}
};
整体代码还是比较简单的, 没有太多弯弯绕绕,就是找自己这个区间最右边界,一直更新,直到追上了.
题目链接
另一种代码思路:统计字符串中所有字符的起始和结束位置,记录这些区间(实际上也就是435.无重叠区间 (opens new window)题目里的输入),将区间按左边界从小到大排序,找到边界将区间划分成组,互不重叠。找到的边界就是答案。卧槽, 成功把简单问题复杂化了.
确实也是一种思路,第一个函数构造之前题目中的输入的数组. 找到各个字符串所在的区间, 之后的函数就负责找到分割点,一旦发现区间不连续了,就分割. 思路很清晰, 代码就不难,但是确实也是够麻烦.
class Solution {
public:
static bool cmp(vector<int> &a, vector<int> &b) {
return a[0] < b[0];
}
// 记录每个字母出现的区间
vector<vector<int>> countLabels(string s) {
vector<vector<int>> hash(26, vector<int>(2, INT_MIN));
vector<vector<int>> hash_filter;
for (int i = 0; i < s.size(); ++i) {
if (hash[s[i] - 'a'][0] == INT_MIN) {
hash[s[i] - 'a'][0] = i;
}
hash[s[i] - 'a'][1] = i;
}
// 去除字符串中未出现的字母所占用区间
for (int i = 0; i < hash.size(); ++i) {
if (hash[i][0] != INT_MIN) {
hash_filter.push_back(hash[i]);
}
}
return hash_filter;
}
vector<int> partitionLabels(string s) {
vector<int> res;
// 这一步得到的 hash 即为无重叠区间题意中的输入样例格式:区间列表
// 只不过现在我们要求的是区间分割点
vector<vector<int>> hash = countLabels(s);
// 按照左边界从小到大排序
sort(hash.begin(), hash.end(), cmp);
// 记录最大右边界
int rightBoard = hash[0][1];
int leftBoard = 0;
for (int i = 1; i < hash.size(); ++i) {
// 由于字符串一定能分割,因此,
// 一旦下一区间左边界大于当前右边界,即可认为出现分割点
if (hash[i][0] > rightBoard) {
res.push_back(rightBoard - leftBoard + 1);
leftBoard = hash[i][0];
}
rightBoard = max(rightBoard, hash[i][1]);
}
// 最右端
res.push_back(rightBoard - leftBoard + 1);
return res;
}
};