文章目录
- [二分库函数 ranges::lower_bound](#二分库函数 ranges::lower_bound)
-
-
- [1. 最直观的对比](#1. 最直观的对比)
- [2. 在 LIS 中的完整用法](#2. 在 LIS 中的完整用法)
- [3. 为什么叫 "lower_bound"?](#3. 为什么叫 "lower_bound"?)
- [4. 相比旧版 `std::lower_bound` 的优势](#4. 相比旧版
std::lower_bound的优势) -
- [💡 投影功能(高级但实用)](#💡 投影功能(高级但实用))
- [5. ⚠️ 面试注意事项](#5. ⚠️ 面试注意事项)
- [📌 一句话总结](#📌 一句话总结)
-
- [捕获 lambda【&】](#捕获 lambda【&】)
-
-
- [1. 三种最常见的捕获方式对比](#1. 三种最常见的捕获方式对比)
- [2. 用一张表记住](#2. 用一张表记住)
- [3. 为什么面试建议用 `\&ls` 而非 `\&`?](#3. 为什么面试建议用
[&ls]而非[&]?) - [4. ⚠️ `\&` 的经典坑](#4. ⚠️
[&]的经典坑) - [📌 一句话总结](#📌 一句话总结)
-
二分库函数 ranges::lower_bound
ranges::lower_bound 是 C++20 引入的 <algorithm> 库中的函数,它是传统 std::lower_bound 的现代升级版。
别被名字吓到,它的核心功能非常简单:在一个有序序列中,二分查找第一个"大于等于"目标值的位置。
1. 最直观的对比
假设你有一个严格递增数组 g = [2, 4, 6, 8],你要找 x = 5:
cpp
// ✅ C++20 新写法(简洁、安全)
auto it = ranges::lower_bound(g, 5);
// it 指向 6(第一个 >= 5 的元素)
// ❌ C++17 旧写法(冗长、易错)
auto it = std::lower_bound(g.begin(), g.end(), 5);
// 效果完全相同,但要手写 begin/end
如果 x = 9(比所有元素都大),it == g.end(),表示没找到。
2. 在 LIS 中的完整用法
回到你之前的代码,逐行拆解:
cpp
vector<int> g; // g 始终保持严格递增
for (int x : nums) {
// 🔍 在 g 中二分查找第一个 >= x 的位置
auto it = ranges::lower_bound(g, x);
if (it == g.end()) {
// x 比 g 中所有元素都大 → LIS 可以延长
g.push_back(x);
} else {
// 找到了 >= x 的元素 → 用更小的 x 替换它
// 这样未来有更多机会延长 LIS
*it = x;
}
}
return g.size(); // g 的长度就是 LIS 长度
3. 为什么叫 "lower_bound"?
这个名字来源于数学概念:
| 术语 | 含义 | 对应 STL 函数 |
|---|---|---|
| Lower Bound | 第一个 ≥ target 的位置 | ranges::lower_bound |
| Upper Bound | 第一个 > target 的位置 | ranges::upper_bound |
记忆口诀:
- Lower = ≥ (下界,包含自身)
- Upper = > (上界,不包含自身)
在 严格递增 LIS 中,我们用 lower_bound(因为相等元素不能延长 LIS,需要替换)。如果题目改为非递减 LIS ,则改用 upper_bound。
4. 相比旧版 std::lower_bound 的优势
| 特性 | std::lower_bound (C++98) |
ranges::lower_bound (C++20) |
|---|---|---|
| 传参方式 | 必须传 begin, end 迭代器对 |
直接传容器/range |
| 安全性 | 容易传错 begin/end 导致 UB | 编译期检查,不可能传错 |
| 可读性 | lower_bound(v.begin(), v.end(), x) |
ranges::lower_bound(v, x) |
| 投影支持 | 不支持 | ✅ 支持第三参数自定义比较 |
| 时间复杂度 | O(log N) | O(log N)(完全相同) |
💡 投影功能(高级但实用)
cpp
struct Student { int score; string name; };
vector<Student> students = {{90,"A"}, {80,"B"}, {70,"C"}};
// 按 score 排序后,直接按 score 字段二分查找
auto it = ranges::lower_bound(students, 85, {}, &Student::score);
// 无需写 lambda,第三个参数 {} 表示默认比较,第四个是投影
5. ⚠️ 面试注意事项
| 场景 | 建议 |
|---|---|
| 面试官允许 C++20 | ✅ 放心用 ranges::lower_bound |
| 不确定 C++ 版本 | ⚠️ 用 std::lower_bound(g.begin(), g.end(), x) |
| 面试官问区别 | 回答:"语义相同,ranges 版本更安全、更简洁,是 C++20 的改进" |
| 手撕二分环节 | ✍️ 两者都不要用,手写标准二分模板 |
📌 一句话总结
ranges::lower_bound(container, value)= 在有序容器中二分找第一个 ≥ value 的位置 。它就是std::lower_bound的语法糖 + 安全加固版,LIS 中用它来维护贪心数组,把 O ( N 2 ) O(N^2) O(N2) 优化到 O ( N log N ) O(N \log N) O(NlogN)。
如果你当前环境不支持 C++20,记住等价替换即可,逻辑完全一致:
cpp
// C++20
auto it = ranges::lower_bound(g, x);
// C++17 及以下(完全等价)
auto it = lower_bound(g.begin(), g.end(), x);
Lower = ≥ (下界)
Upper = > (上界)
想要 < → 用 lower_bound 然后 -1
想要 ≤ → 用 upper_bound 然后 -1
永远记得检查边界(it == begin() 时减一会越界!)
| 你想找的 | 数学含义 | STL 函数 | C++20 写法 | C++17 写法 |
|---|---|---|---|---|
| 第一个 ≥ x | lower bound | lower_bound |
ranges::lower_bound(g, x) |
std::lower_bound(g.begin(), g.end(), x) |
| 第一个 > x | upper bound | upper_bound |
ranges::upper_bound(g, x) |
std::upper_bound(g.begin(), g.end(), x) |
| 第一个 < x | ❌ 无直接函数 | 用 lower_bound 反推 |
见下方 👇 | 见下方 👇 |
| 第一个 ≤ x | ❌ 无直接函数 | 用 upper_bound 反推 |
见下方 👇 | 见下方 👇 |
捕获 lambda【&】
在 C++ 的 Lambda 表达式中,[] 里的内容叫做捕获列表(Capture List),它决定了 Lambda 如何访问外部作用域的变量。
[&] 的意思是:按引用(by reference)捕获外部所有自动存储期变量。
1. 三种最常见的捕获方式对比
cpp
int a = 10;
int b = 20;
// ✅ [&] 按引用捕获所有外部变量
auto f1 = [&]() { a++; }; // 修改的是外部的 a
f1();
cout << a; // 输出 11 ← 外部 a 被改了
// ✅ [=] 按值(拷贝)捕获所有外部变量
auto f2 = [=]() mutable { a++; }; // 修改的是内部副本
f2();
cout << a; // 输出 10 ← 外部 a 不变
// ✅ [&ls] 只按引用捕获 ls,其他变量不可访问
auto f3 = [&ls](int x) { ... }; // 精确控制,面试推荐
2. 用一张表记住
| 写法 | 含义 | 能否修改外部变量 | 安全性 |
|---|---|---|---|
[&] |
按引用捕获所有 | ✅ 能 | ⚠️ 低:可能意外修改不该改的变量 |
[=] |
按值拷贝所有 | ❌ 不能(除非加 mutable) |
⚠️ 中:大对象拷贝有性能开销 |
[&var] |
只按引用捕获 var |
✅ 仅 var 能 |
✅ 高:意图明确 |
[var] |
只按值拷贝 var |
❌ 不能 | ✅ 高 |
[this] |
捕获当前对象指针 | ✅ 通过 this-> |
⚠️ 注意生命周期 |
[&a, b] |
混合:a 引用,b 拷贝 | 仅 a 能 | ✅ 高 |
3. 为什么面试建议用 [&ls] 而非 [&]?
回到你的 LIS 代码:
cpp
// ❌ [&] ------ 捕获了 nums, n, i, findindex 等所有变量
// 面试官会想:"你真的需要捕获这么多吗?还是只是懒得写?"
auto findindex = [&](int x) -> int { ... };
// ✅ [&ls] ------ 只捕获了二分查找真正需要的数组
// 面试官看到就知道:你清楚这个 lambda 只依赖 ls
auto findIndex = [&ls](int x) -> int { ... };
核心区别不是功能,而是表达力:显式捕获 = 告诉面试官"我思考过这个 Lambda 的数据依赖边界"。
4. ⚠️ [&] 的经典坑
cpp
vector<function<int()>> funcs;
for (int i = 0; i < 5; i++) {
funcs.push_back([&]() { return i; }); // 💥 按引用捕获了循环变量 i
}
for (auto& f : funcs) cout << f() << " ";
// 输出:5 5 5 5 5 (全是 5!因为 i 已经变成 5 了)
如果改成 [i](按值捕获),每个 Lambda 会保存自己那份 i 的副本,输出就是 0 1 2 3 4。
📌 一句话总结
[&]= "把外面所有变量都借进来用(可改)"
[&ls]= "我只借ls这一个变量来用"功能上
[&]没问题,但面试中写[&ls]是在展示你对数据流的精确掌控。