406.根据身高重建队列
这题的思想和分糖果那题的其中一个类似,当有多种条件需要考虑,那么一个一个来解决,一块思考容易顾此失彼。遇到两个维度权衡的时候,一定要先确定一个维度,再确定另一个维度。
假设候选队列为 A,已经站好队的队列为 B。从 A 里挑身高最高的人 x 出来,插入到 B.。因为 B 中每个人的身高都比 x 要高,因此 x 插入的位置,就是看 x 前面应该有多少人就行了。比如 x 前面有 5 个人,那 x 就插入到队列 B 的第 5 个位置。
一般这种数对,还涉及排序的,根据第一个元素正向排序,根据第二个元素反向排序,或者根据第一个元素反向排序,根据第二个元素正向排序,往往能够简化解题过程。
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];
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort(people.begin(), people.end(), cmp);
vector<vector<int>> queue;
for(int i = 0; i < people.size(); i++){
int pos = people[i][1];
queue.insert(queue.begin() + pos, people[i]);
}
return queue;
}
};
由于vector的插入时间复杂度很高,可以使用链表存储进行优化
class Solution {
public:
// 身高从大到小排(身高相同k小的站前面)
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];
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort (people.begin(), people.end(), cmp);
list<vector<int>> que; // list底层是链表实现,插入效率比vector高的多
for (int i = 0; i < people.size(); i++) {
int position = people[i][1]; // 插入到下标为position的位置
std::list<vector<int>>::iterator it = que.begin();
while (position--) { // 寻找在插入位置
it++;
}
que.insert(it, people[i]);
}
return vector<vector<int>>(que.begin(), que.end());
}
};
452. 用最少数量的箭引爆气球
接下来的3题都是重叠区间的问题
想象中,气球是按照区间从大到小摆放的,但数据给的顺序不是,为了方便查找相邻的区间,需要将数组进行排序,使得气球的区间的顺序也是按照从大到小的顺序排列的。
这里的排序有两种方法,第一种是按start排序,排出来的上述的正常思维的顺序。
主要思路是如果两个区间完全不挨着,那肯定得多用一根箭;如果两个区间挨着就不用多加一根,但3个及以上区间挨着时就一定要注意挨着的这些区间的最小end,如果下一个区间的start大于这个最小end,那即使区间挨着也得多加一根箭。
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b){
return a[0] < b[0];
}
int findMinArrowShots(vector<vector<int>>& points) {
sort(points.begin(), points.end(), cmp);
int num = 1;
int lastright = points[0][1];
for(int i = 1; i < points.size(); i++){
if(points[i][0] > lastright){//不相交的条件:后一个气球的开始坐标大于(不能等于)前一个气球的结束坐标
num++;
lastright = points[i][1];
}
else{//如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭。
lastright = min(points[i][1], lastright);
}
}
return num;
}
};
另一种思路,按end排序。
如果用end排序,那么一个最大的好处就是区间列表里第一项的end是最小的end,这样就不用再上上文提到的那样去找最小end了。在end排序下,只有一种情况需要多加箭,那就是next_start > cur_end时,其他情况统统都是一支箭搞定,这样从代码上还是思路上都简化了不少。
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 num = 1;
int lastright = points[0][1];
for(int i = 1; i < points.size(); i++){
if(points[i][0] > lastright){//不相交的条件:后一个气球的开始坐标大于(不能等于)前一个气球的结束坐标
num++;
lastright = points[i][1];
}
}
return num;
}
};
435. 无重叠区间
思路和上一题一样,代码也相似,虽然都是更新右区间,但是含义是不相同的,要想明白这题代码里删除的到底是哪个。
class Solution {
public:
static bool cmp(vector<int>& a, vector<int>& b){
return a[0] < b[0];
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(), cmp);
int result = 0;
for(int i = 1; i < intervals.size(); i++){
if(intervals[i][0] < intervals[i-1][1]){
result++;
intervals[i][1] = min(intervals[i][1], intervals[i-1][1]);
//因为找到需要移除区间的最小数量,所以选择删除右区间更大的,使得和下一个重叠的可能性减少
//关键在于贪心:若有重叠,移除右边界大的那个区间,代码上体现在更新当前区间的右边界
}
}
return result;
}
};
这题的题解里找到了一个关于为什么采取右端排序更好的解释,写的很好的,记录一下
关于为什么是推荐按照区间右端点排序?
官解里对这个描述的非常清楚了,这个题其实是预定会议的一个问题,给你若干时间的会议,然后去预定会议,那么能够预定的最大的会议数量是多少?核心在于我们要找到最大不重叠区间的个数。 如果我们把本题的区间看成是会议,那么按照右端点排序,我们一定能够找到一个最先结束的会议,而这个会议一定是我们需要添加到最终结果的的首个会议。(这个不难贪心得到,因为这样能够给后面预留的时间更长)。
对于按照区间左端点排序,当两个比较的区间存在重叠时,再比较区间右端点的大小,保留右端点小的区间(对应结束时间早的区间),这样能够满足剩余非重叠区间的个数最多。
对于按照区间右端点排序,当两个比较的区间存在重叠时,无需比较右端点的大小, 因为按照右端点排序, 后者肯定大于前者,因此只需保留右端点(前者)小的区间(对应结束时间早的区间)。
综上所述:按照左区间排序比按照右区间排序多了一步比较两区间右端点大小,选出右端点小的区间的步骤。
763.划分字母区间
这题我没有采用之前区间重叠的思路,我的思路是用双指针去找每个字符最后出现的位置,然后维护一个最大的右边界值,当 i = right 的时候,说明 right 之前的所有字符已经不会出现在右边了,可以分割了。
class Solution {
public:
vector<int> partitionLabels(string s) {
int right = 0;
int left = 0;
vector<int> result;
for(int i = 0; i < s.size(); i++){
for(int j = s.size()-1; j > 0; j--){
if(s[j] == s[i]){
right = max(j, right);
break;
}
}
if(i == right){
result.push_back(right+1 - left);
left = right + 1;
right = 0;
}
}
return result;
}
};
这个思路的问题就在于慢,每个字符都重新找了一遍右边界,但其实有很多重复的操作。
可以将里面的for循环提取出来,先找区间内所有种类字符的右边界在哪,存起来,再遍历一次找切割点。
class Solution {
public:
vector<int> partitionLabels(string s) {
//创建哈希表来存储我们记录到字符串中的元素的最后下标
int hash[27] = {0};
//遍历字符串,将最后一次出现元素记录放入哈希表中
for(int i=0;i<s.size();i++){
hash[s[i]-'a'] = i;
}
//创建结果容器,用于返回
vector<int>ans;
//创建左右指针
int right = 0;
int left = 0;
//再次遍历字符串,确定我们的断点
for(int j=0;j<s.size();j++){
//找到出现最远的字符,得到它的下标
right = max(right,hash[s[j]-'a']);
if(right == j){
//将结果放入容器中,别忘了字符串从是零开始的,所以结果+1
ans.push_back(right - left + 1);
//找到边界后移动左指针,以后续用于确定新边界
left = j + 1;
}
}
//返回
return ans;
}
};