值域线段树:查找第一个小于阈值的位置
值域线段树查找第一个小于阈值的位置:线段树上二分。
一句话算法
把每个数值当成桶,桶里放这个数第一次出现的位置;要找第一个小于
问题模型
给定一个长度为
第一个满足 a[i] < x 的位置 i 是多少?
如果不存在这样的位置,输出
例如数组为:
下标: 1 2 3 4 5 6 7
数值:10 5 8 4 5 9 3
询问 x = 5 时,小于 5 的位置有 4,7,第一个位置是 4。
单次询问直接从左到右扫描是
核心直觉
普通线段树通常维护“下标区间”。这道题换一个角度:维护“值域区间”。
对每个数值
若
那么:
也就是说,原问题从“在数组里找位置”变成了“在值域里查询一段桶的最小值”。
为什么需要离散化
如果数组值很大,例如
我们只关心数组中真正出现过的值,所以先把所有值排序去重:
原值: 3 4 5 8 9 10
压缩编号: 1 2 3 4 5 6
询问小于 lower_bound 找到最后一个 < x 的压缩编号,然后在线段树上查询 [1,last_less] 的最小位置。
算法步骤
- 读入数组,把所有数放进
values。 - 对
values排序去重,得到离散化后的值域。 - 建一棵维护最小值的线段树,初始值为
INF。 - 从左到右扫描数组:
- 找到
a[i]的压缩编号rank; - 用
i更新这个桶的最小值。
- 找到
- 回答询问
x:last_less = lower_bound(values, x) - values.begin();- 查询压缩值域
[1,last_less]的最小下标; - 如果结果为
INF,输出n+1。
算法证明
关键不变量:线段树的每个叶子维护某个数值第一次出现的位置,内部节点维护它所代表值域中所有叶子的最小位置。
预处理时,数组从左到右扫描。对值 min(当前值, i),所以扫描结束后叶子正好等于
对询问
所以:
线段树查询的正是这个值域区间内的最小 first,因此答案正确。
复杂度分析
设数组长度为
- 离散化:
。 - 建立值域线段树:
。 - 每次询问:
。 - 空间复杂度:
。
如果只有一次询问,线性扫描
代码实现
模板输入格式:
n q
a1 a2 ... an
x1
x2
...
xq
每个询问输出第一个满足 a[i] < x 的位置;不存在时输出 n+1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <bits/stdc++.h>
using namespace std;
struct MinSegmentTree {
static const int INF = 1e9;
int n = 0;
vector<int> tree;
MinSegmentTree(int n = 0) {
init(n);
}
void init(int size) {
n = size;
tree.assign(n * 4 + 5, INF);
}
void pull(int p) {
tree[p] = min(tree[p << 1], tree[p << 1 | 1]);
}
void update_min(int pos, int value, int l, int r, int p = 1) {
if (l == r) {
tree[p] = min(tree[p], value);
return;
}
int mid = (l + r) >> 1;
if (pos <= mid) update_min(pos, value, l, mid, p << 1);
else update_min(pos, value, mid + 1, r, p << 1 | 1);
pull(p);
}
int query_min(int ql, int qr, int l, int r, int p = 1) const {
if (ql > qr) return INF;
if (ql <= l && r <= qr) return tree[p];
int mid = (l + r) >> 1;
int answer = INF;
if (ql <= mid) answer = min(answer, query_min(ql, qr, l, mid, p << 1));
if (qr > mid) answer = min(answer, query_min(ql, qr, mid + 1, r, p << 1 | 1));
return answer;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, q;
cin >> n >> q;
vector<int> a(n + 1);
vector<int> values;
values.reserve(n);
for (int i = 1; i <= n; i++) {
cin >> a[i];
values.push_back(a[i]);
}
sort(values.begin(), values.end());
values.erase(unique(values.begin(), values.end()), values.end());
MinSegmentTree seg((int)values.size());
for (int i = 1; i <= n; i++) {
int rank = lower_bound(values.begin(), values.end(), a[i]) - values.begin() + 1;
seg.update_min(rank, i, 1, seg.n);
}
while (q--) {
int x;
cin >> x;
// 所有小于 x 的值,正好落在压缩值域的 [1, last_less]。
int last_less = lower_bound(values.begin(), values.end(), x) - values.begin();
int pos = seg.query_min(1, last_less, 1, seg.n);
if (pos == MinSegmentTree::INF) cout << n + 1 << '\n';
else cout << pos << '\n';
}
return 0;
}
测试用例
输入:
7 4
10 5 8 4 5 9 3
5
4
11
1
输出:
4
7
1
8
解释:
< 5的第一个位置是4,值为4。< 4的第一个位置是7,值为3。< 11的第一个位置是1。< 1不存在,输出n+1 = 8。
应用分类详解
这个技巧的本质是“在值域上维护位置最值”。看到题目既关心数值大小,又关心下标先后时,可以考虑把信息放到值域线段树里。
一、按值域筛选,返回最早位置
典型模式: 查询第一个小于、大于、不超过、不低于某个阈值的位置。
识别信号: 题目问“最早出现的位置”,但条件写在数值大小上。
核心建模: 每个值维护它第一次出现的位置,值域区间查询最小位置。
| 应用场景 | 经典题目 | 核心思路 |
|---|---|---|
| 第一个小于阈值的位置 | 本页模型 | 查询值域 (-inf,x) 的最小下标 |
| 第一个大于阈值的位置 | 本页变体 | 查询值域 (x,+inf) 的最小下标 |
| 第一个落在值域区间的位置 | 本页变体 | 查询值域 [L,R] 的最小下标 |
二、离线处理静态数组的多次阈值询问
典型模式: 数组不变,询问很多,每次阈值不同。
识别信号: 没有修改操作,查询次数
核心建模: 先预处理所有值第一次出现的位置,之后每个询问只做一次值域区间最小值。
三、值域线段树的入门变形
典型模式: 不维护出现次数,而维护某个值对应的附加信息。
识别信号: 值域是主要维度,叶子不是 count,而是“最早位置、最晚位置、最优代价”等。
核心建模: 只要值域区间的信息可以由左右孩子合并,就可以套线段树。
经典例题
1. 第一个小于阈值的位置
给定静态数组,多次询问第一个小于
2. 第一个大于阈值的位置
把查询区间从 [1,last_less] 改成 [first_greater,V],其中 first_greater = upper_bound(values, x) + 1。
3. 值域区间内最早出现的位置
每次询问给出 [L,R],要求第一个满足