算法学习记录~2023.X.XX~章节DayX~题目号.题目标题 & 题目号.题目标题
452. 用最少数量的箭引爆气球
题目链接
思路1:自己企图考虑所有情况分别处理(未通过)
如上所示,未通过,有的用例测试结果和期望结果差蛮大的,应该考虑少了非常多情况。
结合 134. 加油站 的类似错误经验,之后要尽量避免暴力枚举所有可能发生的情况分别处理,除非逻辑和可能的情况极其简单且确定,否则无论是效率还是准确率都非常难以保证。
代码
并不能通过,只是记录下当时的想法
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 count = 0; //箭数量
int left = 0; //初始左边界
int right = points[0][1]; //初始右边界
for (int i = 0; i < points.size(); i++){
if (points[i][0] >= left && points[i][0] <= right && i != points.size() - 1){ //当前气球左边界在区间内
if (points[i][1] < right){ //更新最小右区间
right = points[i][1];
}
}
else if (points[i][0] > right){ //超过了右区间,更新区间
count++; //进入到了下一个区间,给上一区间加一箭
left = points[i][0];
right = points[i][1];
if (i == points.size() - 1) //单独扎最后一个
count++;
}
else if (i == points.size() - 1)
count++;
}
return count;
}
};
思路2:贪心
局部最优:当气球出现重叠,一起射,所用弓箭最少
全局最优:把所有气球射爆所用弓箭最少
为了让气球尽可能重叠,对数组进行排序。按照开始位置或结束位置排序都可以。
按照开始位置排序的话,就要从前向后遍历,靠左尽可能让气球重复。
如果气球有重叠,那么重叠气球的右边界的最小值之前的区间一定需要一个弓箭。
代码
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) {
if (points.size() == 0) //没有气球那自然不需要箭
return 0;
sort(points.begin(), points.end(), cmp);
int result = 1; //points 不为空则至少需要一支箭
for (int i = 1; i < points.size(); i++){
if (points[i][0] > points[i - 1][1]){ //气球 i 和气球 i - 1 不重叠
result++;
}
else{ //气球 i 和气球 i - 1 有重叠
points[i][1] = min(points[i][1], points[i - 1][1]); //更新重叠气球的最小右边界
}
}
return result;
}
};
总结
结合 134. 加油站 的类似错误经验,之后要尽量避免暴力枚举所有可能发生的情况分别处理,除非逻辑和可能的情况极其简单且确定,否则无论是效率还是准确率都非常难以保证。
至于本题思路,比较好想到尽可能让更多气球重叠在一起,但是模拟处理的具体过程不是很好想,代码也不是很好写。
435. 无重叠区间
题目链接
思路1:排序右边界
如果不重叠,那么每个区间一定都有个右边界。
因此先排序右边界,这样先初始化当前区间边界值 right 为第一个数的右边界,接着从前向后遍历,如果左边界小于当前区间边界值 right ,那说明这个区间一定是重叠的,就把重叠计数 count 加1。
如果当前遍历到的左区间大于等于当前区间边界值 right,那么说明进入了下一个区间,更新 right 为现在的右边界,继续遍历
代码
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 result = 0;
int right = intervals[0][1]; //最小右边界,初始化为第一个数的右边界
for(int i = 1; i < intervals.size(); i++){
if (intervals[i][0] < right){ //发生重叠,删除掉这个区间
result ++;
}
else{ //到下一个区间了,更新最小右边界
right = intervals[i][1];
}
}
return result;
}
};
思路2:排序左区间
其实具体想法和上面没啥区别,就是在遍历处理的逻辑上有一定区别。
主要就是对于每一个区间,如果发现了重叠,需要取已经有的 right 和当前遍历到的右区间的最小值,也就是发现重叠时要不断更新最小右区间保证确定为最小,这样才能尽可能少删除,因为能让后面的区间能更往前一些。
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) {
sort(intervals.begin(), intervals.end(), cmp);
int result = 0;
int right = intervals[0][1]; //最小右边界,初始化为第一个数的右边界
for(int i = 1; i < intervals.size(); i++){
if (intervals[i][0] < right){ //发生重叠,删除掉这个区间
right = min(right, intervals[i][1]); //这个很关键,需要不断重置重叠区间的最小右边界
result ++;
}
else{ //到下一个区间了,更新最小右边界
right = intervals[i][1];
}
}
return result;
}
};
总结
一开始是按照左区间排序,但是判断时用的右区间排序才能实现的逻辑,这样就会发现需要考虑非常多特殊情况,也不是很对。
其实碰到好几次这种题目了,多维度时不同排序方式有可能也对应着不同的遍历方向或者思路,比较容易考虑不清楚。
763.划分字母区间
题目链接
思路1:自己想的,和上一题类似
把每一个个字母都抽象成一个区间,最左和最右的该字母就是区间的左右边界。
这样就可以把问题转换成如何获得最多的不重叠区间。但是因为所有区间都是固定的,如果想获得最多的不重叠区间,那其实就是尽量删除最少的区间来使剩余区间不重叠,这样就和 435. 无重叠区间 完全一样了。
具体处理思路和上一题 435. 无重叠区间 相同,区别在于本题多了一步要把原来的字符串先转化为一个个区间。
(后来觉得可能不太一样,因为那个如果删除一个区间,那那个区间的右区间就失效了,也就是说右区间可以尽可能小,如果发生重叠,删除掉更大的以后剩下的右区间会更小。而本题并不能删除区间,因此需要取最大的右区间而不是最小。)
(所以把之前记录最小右边界改为记录最大右边界那确实答案就一样了,carl哥在后面也提到了这个思路,写文章时一边做一边写的,刚才没有看全,现在补充上)
代码
没有自己写,直接借用carl哥的
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;
}
};
思路2:也不是很算贪心
其实大致思路相似,因为可以当做这个字符串是已经按左区间排好序的很多个区间,这样找最远的右区间就可以了,和上一题的区别就是因为不能删除区间,因此应该记录的是最大右区间而不是最小右区间,也就是让重叠的都一起重叠了,不再占用下一个区间了。
具体处理思路就是,第一次从前向后遍历,找每个字母的最远边界。
第二次从前向后遍历,不断更新最远边界,取当前遍历的字母的最远区间和当前记录好的最远区间的最大值,如果遍历到了记录的最远区间最大值,说明已经到了重复区间的右节点,前面那些就是要求的区间范围了,接着继续下一个。
代码
class Solution {
public:
vector<int> partitionLabels(string s) {
int hash[26] = {0}; //i 为字符,hash[i] 就是字符的最远位置
for (int i = 0; i < s.size(); i++){ //记录字符最远位置
hash[s[i] - 'a'] = i;
}
vector<int> result;
int left = 0;
int right = 0;
for (int i = 0; i < s.size(); i++){
right = max(right, hash[s[i] - 'a']); //更新最大右边界
if (i == right){ //找到了一个区间
result.push_back(right - left + 1); //记录分好的区间的长度
left = i + 1; //更新left为下一个区间的开始
}
}
return result;
}
};
总结
虽然自己想的思路应该是有问题的,不过这种能意识到或许能转化为已经掌握的知识的感觉还是挺让人振奋的。
56. 合并区间
题目链接
思路
跟 452. 用最少数量的箭引爆气球 和 435. 无重叠区间 没什么本质区别,都是要判断区间重叠,区别就在于判断出重叠后的后续处理。
因此本题思路就是先按照左边界或右边界进行排序。
比如按左边界从小到大排序,如果 intervals[i][0] <= intervals[i - 1][1] 即intervals[i]的左边界 <= intervals[i - 1]的右边界,则一定有重叠(本题相邻区间也算重贴,所以是<=)
知道如何判断重复之后,剩下的就是合并了,如何去模拟合并区间呢?
其实就是用合并区间后左边界和右边界,作为一个新的区间,加入到result数组里就可以了。如果没有合并就把原区间加入到result数组。
(关于合并的代码可以学习一下,以前没有用过直接取 vector 数组的 back 取值)
代码
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> 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]); //第一个区间直接放进结果集,后面如果有重叠就在result上直接合并
for (int i = 1; i < intervals.size(); i++){
if (intervals[i][0] <= result.back()[1]){ //有重叠,需要合并(这个的写法学习一下)
//合并区间,只更新右边界就好,因为result.back()的左边界一定是最小值,因为我们按照左边界排序的
result.back()[1] = max(result.back()[1], intervals[i][1]);
}
else{ //区间不重叠,压入下一个区间
result.push_back(intervals[i]);
}
}
return result;
}
};
总结
今天的几道题都有很好的关联性,具体区别也就是判断重叠后的后续处理,逐渐能够找到一些感觉了。