【题目描述】
已知有数列 a0,a1,...,aN−1
有 M 次查询 Query(x,y)
对于每个 Query(x,y) 求 ax,ax+1,...,ay 范围内的最小值
【输入格式】
第一行: N
第二行:数列 a ,共 N 个数
第三行: M
第四行 ~ 第M+3行: x , y
【输出格式】
对于每个 x , y ,输出 ax,ax+1,...ay 范围内的最小值
每个值单独成行
【数据范围】
N≤5×106,M≤106
(略)
建表时间复杂度是 O(N2) ;
每次查询的时间复杂度是 O(1) 。
(略,有时间再补)
基于稀疏表的算法是一个很有名的在线算法(就是读入一组询问就立即处理并输出,同样有离线算法,就是读入所有询问后重新组织查询处理顺序)。
在程序中, f[i][j] 表示 min(a[j],a[j+1],...,a[j+2i−1])
有很多人把ST当作动态规划,貌似也可以,不过比较权威的书上将其归入ST里。
在本题中,ST建表的时间复杂度为 O(NlogN) ;
每次查询的时间复杂度为 O(1) 。
稀疏表的空间复杂度为 O(NlogN) 。
#include <iostream>
#include <climits>
using namespace std;
int f[18][500000];
int main() {
int N;
cin>>N;
/*打表和初始化*/
int lg[N + 1]; lg[1] = 0;
for (int i = 2; i <= N; i++)
lg[i] = lg[i >> 1] + 1; //给log2打表
int table_depth = lg[N];
for (int i = 1; i < table_depth; i++)
for (int j = 0; j < N; j++)
f[i][j] = INT_MAX;
for (int i = 0; i != N; i++)
cin >> f[0][i];
/*建表*/
int right_bound;
for (int i = 1; i<= table_depth; i++) {
right_bound = N - (1 << i) + 1;
for (int j = 0; j < right_bound; j++)
f[i][j] = min(f[i - 1][j], f[i - 1][j + (1 << (i - 1))]);
}
/*查询*/
int M, x, y, query_depth;
cin>>M;
for (int i = 0; i != M; i++) {
cin >> x >> y;
if (x == y) {
cout << f[0][x] << endl;
continue;
}
query_depth = lg[y-x];
cout << min(f[query_depth][x], f[query_depth][y - (1 << query_depth) + 1]) << endl;
}
return 0;
}
我的查询部分还不是很完善,求教大犇……
话说线段树的缩写也叫ST,不过通常说ST都是指基于稀疏表的算法,呵呵。
基于线段树的实现比ST有个好处:基于线段树,可以增加区间修改操作,即
给定 i, j 和 b,把 ai,ai+1,...,aj 的值加b。
用ST实现也不是不行,但是修改效率很低—— O(NlogN) ,比朴素算法都慢。
#include <iostream>
using namespace std;
const int maxN = 1<<17;
const int oo = 0x7fffffff;
struct Tnode
{
bool s;
int delta;
int l, r; //左右范围
int min;
int lc, rc; //左孩子和右孩子的位置
Tnode()
{
s = false;
lc = rc = delta = 0;
}
};
Tnode f[maxN];
int fp=0;
/*----------------申请节点----------------*/
int getPoint(int l, int r, int min)
{
fp++;
f[fp].l = l;
f[fp].r = r;
f[fp].min = min;
return fp;
}
/*----------------建树----------------*/
int create(int l, int r)
{
int now = getPoint(l, r, oo); //申请节点
if (l<r)
{
f[now].lc = create(l, (l+r)/2); //递归:建左子树
f[now].rc = create((l+r)/2+1, r); //递归:建右子树
}
return now;
}
void push(int root)
{
if (!f[root].s) return;
int delta = f[root].delta;
if (f[root].lc != 0)
{
f[f[root].lc].s = true;
f[f[root].lc].delta += delta;
}
if (f[root].rc != 0)
{
f[f[root].rc].s = true;
f[f[root].rc].delta += delta;
}
if (f[root].rc == 0 && f[root].lc == 0)
f[root].min += delta;
f[root].s = false;
f[root].delta = 0;
}
/*----------------更新----------------*/
void update(int root, int l, int r, int d) //d指增量
{
push(root);
if (l==f[root].l && r==f[root].r)
{
f[root].s = true;
f[root].delta += d;
return;
}
int mid = (f[root].l+f[root].r) / 2;
if (l <= mid) update(f[root].lc, l, min(mid,r), d);
if (r >= mid+1) update(f[root].rc, max(mid+1,l), r, d);
}
/*----------------查询----------------*/
int query(int root, int l, int r)
{
push(root);
int nowl = f[root].l, nowr = f[root].r;
if (l>nowr || r<nowl) return oo; //如果所查询区间与当前区间没有交集就返回无(oo)
if (l<=nowl && r>=nowr) return f[root].min; //如果当前区间完全包含所查询区间就返回该区间的最小值
int mid = (nowl+nowr) / 2;
int lmin = query(f[root].lc, l, min(mid,r));
int rmin = query(f[root].rc, max(mid+1,l), r);
return min(lmin, rmin);
}
int main()
{
int n, q, root;
cin >> n >> q;
root = create(1,n);
char opr; //操作符
int l, r, d;
for (int i=0; i<q; i++)
{
cin >> opr;
if (oper == 'Q')
{
cin >> l >> r;
cout << query(root, l, d) << endl;
}
if (opr == 'U')
{
cin >> l >> r >> v;
update(root, l, r, v);
}
}
return 0;
}
线段树建树的时间复杂度为 O(N) ;
每次查询的时间复杂度为 O(logN) ;
每次修改的时间复杂度为 O(logN) 。
线段树的空间复杂度为 O(N) 。
这是因为线段树的节点数为 N+N2+N4+N8+...=2N−1=O(N) 。
时间复杂度:
操作 | 朴素算法 | ST | 线段树 | 平方分割 |
---|---|---|---|---|
预处理 | O(N2) | O(NlogN) | O(N) | O(N) |
查询 | O(1) | O(1) | O(logN) | O(N−−√) |
空间复杂度:
朴素算法 | ST | 线段树 | 平方分割 |
---|---|---|---|
O(N2) | O(NlogN) | O(N) | O(N) |
在这种难度下,朴素算法已跪(跪在空间范围上)
时间复杂度:
操作 | ST | 线段树 | 平方分割 |
---|---|---|---|
预处理 | O(NlogN) | O(N) | O(N) |
查询 | O(1) | O(logN) | O(N−−√) |
更新 | O(NlogN) | O(logN) | O(N−−√) |
空间复杂度:
朴素算法 | ST | 线段树 | 平方分割 |
---|---|---|---|
O(N2) | O(NlogN) | O(N) | O(N) |
时间复杂度:
操作 | 朴素算法 | ST | 线段树 | 平方分割 |
---|---|---|---|---|
预处理 | – | O(NlogN) | O(N) | O(N) |
查询 | O(1) | O(logN) | O(N−−√) | |
更新 | O(NlogN) | O(logN) | O(N−−√) |
空间复杂度:
朴素算法 | ST | 线段树 | 平方分割 |
---|---|---|---|
O(N2) | O(NlogN) | O(N) | O(N) |
总体来说,基于线段树的实现比基于ST的实现更高效,比平方分割法更省空间,当然,线段树的编程难度最大。
资料5中讲了一种用笛卡尔树的方法说是 O(N) 打表 O(1) 查询(不能修改)╮(╯▽╰)╭
参考资料:
1. Wikipedia. Range minimum query [link]
2. 维基百科. 线段树 [中文link] [英语Link] [俄语Link]
3. Topcoder. Data Science Tutorials. Range Minimum Query and Lowest Common Ancestor [link]
4. Stanford. CS166. Range Minimum Queries, Part One [link]
5. Stanford. CS166. Range Minimum Queries, Part Two [link]
6. GeeksforGeeks. Advanced Data Structure. Segment Tree. Set 2 (Range Minimum Query) [link]
7. GeeksforGeeks. Advanced Data Structure. Segment Tree. Lazy Propagation in Segment Tree) [link]
8. 董的博客. 数据结构与算法. 算法之LCA与RMQ问题 [link]
9. 本站@飘过的小牛. RMQ (Range Minimum/Maximum Query)算法 [link]
10. 本站@忆梦. 线段树 [link]