范围最小值查询 RMQ (Range Minimum Query)

一、最简单的RMQ:静态

【题目描述】
已知有数列 a0,a1,...,aN1
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 范围内的最小值
每个值单独成行
【数据范围】
N5×106M106

0. 朴素的DP

(略)
建表时间复杂度是 O(N2)
每次查询的时间复杂度是 O(1)

1. 平方分割/块分割(Sqrt Decomposition / Block Decomposition)

(略,有时间再补)

2. 稀疏表(ST,Sparse Table)

基于稀疏表的算法是一个很有名的在线算法(就是读入一组询问就立即处理并输出,同样有离线算法,就是读入所有询问后重新组织查询处理顺序)。
在程序中, f[i][j] 表示 min(a[j],a[j+1],...,a[j+2i1])
有很多人把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;
}

我的查询部分还不是很完善,求教大犇……

3. 线段树(Segment Tree)

话说线段树的缩写也叫ST,不过通常说ST都是指基于稀疏表的算法,呵呵。
基于线段树的实现比ST有个好处:基于线段树,可以增加区间修改操作,即

给定 i, j 和 b,把 ai,ai+1,...,aj 的值加b。

用ST实现也不是不行,但是修改效率很低—— O(NlogN) ,比朴素算法都慢。

  1. 线段树的基本结构

范围最小值查询 RMQ (Range Minimum Query)_第1张图片

#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+...=2N1=O(N)

4. 复杂度

时间复杂度:

操作 朴素算法 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)

二、进阶:单点修改

在这种难度下,朴素算法已跪(跪在空间范围上)

4. 复杂度

时间复杂度:

操作 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)

三、Hard模式:区间修改

4. 复杂度

时间复杂度:

操作 朴素算法 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]

你可能感兴趣的:(线段树,RMQ,稀疏表,平方分割)