力扣每日一刷Day 15
Practice Day fifteen:Leetcode T435
今天要讲的故事是,正义的骑士,大战邪恶的贪心算法
先来讲讲贪心算法是怎么一回事吧。
事实上贪心算法并不算一种算法,更多的代表一种思想。
核心思想
- 局部最优 → 全局最优:在每一步,只考虑当前情况下的最佳选择,不回溯、不考虑未来步骤的影响。
- 无后效性:过去的选择不会影响未来的决策,每个步骤的决策仅依赖于当前状态。
适用条件
贪心选择性质
全局最优解可以通过一系列局部最优选择(贪心选择)来获得。即,不从整体出发规划,而是在每一步都做当下最好的选择,最终能得到全局最优。最优子结构性质
问题的最优解包含其子问题的最优解。例如,若问题的最优解在第 k 步选择了方案 A,则剩余步骤的解也必须是对应子问题的最优解。
优缺点
优点
- 效率高:通常时间复杂度较低(如 O (nlogn),因常涉及排序)。
- 实现简单:思路直观,无需复杂的动态规划状态转移。
缺点
- 局限性大:仅适用于满足贪心选择性质的问题,否则可能得到错误解。
- 无法回溯:一旦做出选择,无法修改,可能错过全局最优。
这里我偷了些懒,用了AI,但是我不会自检的,有的用,那当然使劲用啊,哈哈哈哈
理解了贪心算法的思想后,来看看今天的题目吧!
题目数据:以二维数组的形式,给了很多个闭区间
题目条件:
①返回重叠区间个数
②对于点重叠,不作处理
这是一个局部问题,显然可以使用贪心算法。
今天我们使用Leetcode官方解答来解决这个问题吧!这次的解法我还是比较满意的。
事实上,这道题目不适用贪心或者动态规划也能做。只需利用bitset容器以及test()函数即可轻易解决问题。但这是贪心专题,我们还是先讲讲贪心算法如何解决问题吧。
int eraseOverlapIntervals(vector<vector<int>>& intervals)
1 好的,我们要考虑他根本没有给我们区间的情况。这种情况理应在一开始就检测,否则程序运行到最后才检测,就拖慢时间了,不合理。
不过事实上,题目数据要求,至少会有一个区间,不存在没有区间的情况。所以这一行代码,其实在这个题目里面是无效的。为了尊重官方解答,我还是把他放上来了。
if (intervals.empty()) {return 0;}
给出的数组为空,直接返回0值就完事了
2 我们希望进行比较的永远是相邻的两个区间,并且这个相邻需要被定义为:结束区间的相邻。以保证我们进行按一定次序进行比较时,不会出现第三个元素插入两个元素之间,但是被误判为没有重叠的情况。
判断两区间是否重叠的条件为:如果当前区间的结束点小于等于下一个区间的起始点,说明区间没有重叠。这无关排序,只要满足这个条件的,都说明,没有重叠。
就像这样[1,5] [3,6]显然按照上面的条件,这两个区间是进行重叠的。但是当我们插入第三个区间,[1,2],此时[1,2]与[3,6]进行比较,是没有重叠的,按照程序的判断,此时应该输出无重叠。但事实上[1,5]和[1,2]进行重叠了,程序没有对此做出反应。
为了杜绝这种情况的发生,所以我们需要进行排序,排序的顺序为,结束点从小到大依次排列。
sort(intervals.begin(), intervals.end(), [](const auto& u, const auto& v) {return u[1] < v[1];});
这个sort()函数的形状不太常见,我希望可以讲解他。
先来看看他的函数原型吧
template< class RandomIt, class Compare >
void sort( RandomIt first, RandomIt last, Compare comp );
各参数含义:
在 C++ 中,随机访问迭代器(Random Access Iterator) 是迭代器类型中的一种,它支持所有迭代器的操作,并且额外提供了随机访问的能力
RandomIt first
:
随机访问迭代器(Random Access Iterator),指向要排序的序列的起始位置(包含该位置元素)。在你的代码中,对应intervals.begin()
。RandomIt last
:
随机访问迭代器,指向要排序的序列的结束位置(不包含该位置元素)。在你的代码中,对应intervals.end()
。Compare comp
:
比较函数(或函数对象),用于定义排序规则。它需要满足以下条件:但是我们发现,最后一个函数好像不太对劲,怎么形状这么猎奇,怎么没有函数名,还有个[]在前面?
这是Lambda表达式
- 接受两个参数(类型与迭代器指向的元素类型一致),即代码中的
const auto& u
和const auto& v
(实际类型为const vector<int>&
)。 - 返回
bool
类型:若第一个参数应排在第二个参数之前,则返回true
;否则返回false
。
在 C++ 中,lambda 表达式是一种匿名函数(没有函数名的函数),可以在需要函数的地方直接定义和使用,通常用于简化代码,尤其是作为算法的回调函数(如排序、遍历等场景)。
[capture](parameters) -> return_type {...}
外部变量(External Variables) 通常指的是在当前代码块(如函数、lambda 表达式、循环体等)外部定义的变量,即不在当前作用域内声明,但可能被当前代码块访问的变量。
[capture]
(捕获列表):
用于捕获外部变量(lambda 表达式所在作用域中的变量),使其能在函数体内使用。常见用法:[]
:不捕获任何外部变量[=]
:按值捕获所有外部变量(只读)[&]
:按引用捕获所有外部变量(可修改)[x, &y]
:按值捕获x
,按引用捕获y
(parameters)
(参数列表):
与普通函数的参数列表类似,用于定义函数的输入参数。-> return_type
(返回类型):
可选部分,指定函数的返回类型。如果函数体中只有一条return
语句,编译器可以自动推断返回类型,此时可省略。{ ... }
(函数体):
函数的具体实现逻辑。
没有例子实在干巴,我们就以这份代码为例子
- 接受两个参数(类型与迭代器指向的元素类型一致),即代码中的
[](const auto& u, const auto& v) {return u[1] < v[1];}
u和v显然是在Lambda表达式之外被定义的,可是并没有在[ ]捕获列表中捕获变量,似乎与我们在上面说的不大一样。
但Lambda表达式比较赖皮的是,由于他在参数列表进行了参数的输入,所以现在u和v被识别为Lambda表达式本身的参数,所以不必使用[ ]进行捕获。
我们发现,在返回类型的部分是没有东西的,这是因为符合了上述使用条件:函数体中只有一条return语句,那么编译器就会自动推断返回类型,可以直接省略。
在函数体部分,由于传入的u和v实际上是迭代器的地址,加上索引后就可以指向迭代器指向元素的参数,这里索引为1,表示的是区间的第二个数字,也就是结束时间。别忘了,数组索引可是0-base的。
现在,来讲解sort函数本身的排列顺序。
C++ 标准库中的 sort
函数默认是从小到大(升序) 排序的,但这取决于排序时使用的比较规则。
当指定第三个参数(如 lambda 表达式、函数指针等)时,sort
会严格按照该规则排序。规则的逻辑是:
- 若比较函数
comp(a, b)
返回true
,则a
会被排在b
前面; - 否则
b
排在a
前面。
现在,使用了sort函数后,较小的结束区间会被排列在前面
3 判断两区间是否重叠的条件为:如果当前区间的结束点小于等于下一个区间的起始点,说明区间没有重叠。
那么,进入for循环遍历比较前,我们需要人为初始化一个对象,使之程序存在比较的对象
int right = intervals[0][1];
接下来还有一个问题:你该如何计算需要删去的区间呢?我们的区间个数是已知的,只是不知道多少个区间重叠。那么只能使用间接的方法了:用无重叠的区间个数减去总区间个数,就是重叠的区间个数了。
所以我们需要一个元素,来为我们记录无重叠区间的个数。由于官方数据规定,必定会有一个元素,所以无重叠区间初始化为1。
int ans = 1;
接着进入for循环遍历判断,当比较完成后,移交比较对象,即把right值赋为当前处理区间的结束点
for (int i = 1; i < n; ++i) {if (intervals[i][0] >= right) {++ans;right = intervals[i][1];}}
当上述程序结束后,输出答案
return n - ans;}
作者:力扣官方题解
链接:https://leetcode.cn/problems/non-overlapping-intervals/solutions/541543/wu-zhong-die-qu-jian-by-leetcode-solutio-cpsb/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。