RMQ问题总结,标准RMQ算法的实现

RMQ问题:对于长度为N的序列,询问区间[L,R]中的最值

RMQ问题的几种解法:

  1. 普通遍历查询,O(1)-O(N)
  2. 线段树,O(N)-O(logN)
  3. DP,O(NlogN)-O(1)
  4. RMQ标准算法,O(N)-O(1)

简单介绍:

  1. 朴素的查询,不需要任何预处理,但结果是没有任何已知的信息可以利用,每次都需要从头遍历到尾。
  2. 线段树,区间问题的神器,用线段树做比起朴素的暴力查询要快得多,关键在于线段树使用了分治思想,利用了区间问题的可合并性。任何一个区间最多只需要logN个线段树上的区间来合并,线段树上的区间总数目为O(N)个,因此只需要O(N)的预处理就可以将查询复杂度降到O(logN)。同时线段树的树状结构使得修改时信息更容易维护。
  3. DP,又叫ST算法,也是利用了分治的思想。任何一个区间都可以由两个小于当前区间长度的最大的长度为2的幂的区间合并而来,于是预处理出每个点开始所有长度为2的幂的区间最值,那么查询时就可以由预处理的信息O(1)得到答案。
  4. RMQ标准算法,利用了神奇的数据结构--笛卡尔树,笛卡尔树将区间最值问题转化为树上两个点的LCA问题,而DFS可以将LCA问题转化为±1RMQ问题,±1RMQ问题又可以利用分块和动态规划的思想来解决。上述所有预处理,包括笛卡尔树的建立、DFS序以及±1RMQ的问题的求解都可以在线性时间内完成,查询时复杂度为O(1)。

标准算法的实现:

  • 结构图:
  • 笛卡尔树的构造算方法:从左至右扫描原序列,并依次插入到笛卡尔树的右链中,使用单调栈复杂度为O(N)。建好树后,key是二查搜索树,value是小根堆。
  • 最小值与LCA:建好树后,区间最小值问题便转化为了LCA问题,下面简单证明一下:

假设现在询问[d, f]的最小值,root为d和f的LCA,由笛卡尔树的性质可知,root是整棵树表示区间的最小值,而[d, f]是其子区间,所以root不可能比[d, f]中的数小,又因为d和f属于root的不同子树(LCA的性质),所以root一定在[d, f]中(笛卡尔树的性质),故对两个点a,b,LCA(a, b)就是[a, b]的最小值,证毕。

  • ±1RMQ问题:相邻两个数相差1或者-1的序列的RMQ问题
  • ±1RMQ问题解法:将原长度为N的序列分成2N/logN块,每块长度为logN/2,将原来的询问分解为块间询问和块内询问。用ST算法在O(N/logN*log(N/logN))=O(N)的时间内处理出块与块之间的区间最值信息,可以在O(1)的时间内解决块与块之间的询问。对于块内的询问,由于每块长度为logN/2,相邻两个数的差不是1就是-1,于是对于区间最值出现的位置,本质不同的状态只有2logN/2=√N个,加上边界,总共状态数为O(√N*logNlogN),利用递推在O(√N*logNlogN)的时间内求出所有状态来,以后可以在O(1)的时间内得到块内任意区间最值的位置。总复杂度为O(N + √N*logNlogN) ≈ O(N)。
  • LCA与±1RMQ的经典转化就不细说了,详见代码

标准RMQ,O(N)-O(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
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
struct PlusMinusOneRMQ {
    const static int N = 223456;
    const static int M = 15;
    int blocklen, block, minv[N], dp[N][20],
        t[N * 20], f[1 << M][M][M], s[N];
    void init(int n) {
        blocklen = max(1, (int)(log(n * 1.0) / log(2.0)) / 2);
        block = n / blocklen + (n % blocklen > 0);
        int total = 1 << (blocklen - 1);
        for (int i = 0; i < total; i ++) {
            for (int l = 0; l < blocklen; l ++) {
                f[i][l][l] = l;
                int now = 0, minv = 0;
                for (int r = l + 1; r < blocklen; r ++) {
                    f[i][l][r] = f[i][l][r - 1];
                    if ((1 << (r - 1)) & i) now ++;
                    else {
                        now --;
                        if (now < minv) {
                            minv = now;
                            f[i][l][r] = r;
                        }
                    }
                }
            }
        }

        int tot = N * 20;
        t[1] = 0;
        for (int i = 2; i < tot; i ++) {
            t[i] = t[i - 1];
            if (!(i & (i - 1))) t[i] ++;
        }
    }
    void initmin(int a[], int n) {
        for (int i = 0; i < n; i ++) {
            if (i % blocklen == 0) {
                minv[i / blocklen] = i;
                s[i / blocklen] = 0;
            }
            else {
                if (a[i] < a[minv[i / blocklen]]) minv[i / blocklen] = i;
                if (a[i] > a[i - 1]) s[i / blocklen] |= 1 << (i % blocklen - 1);
            }
        }
        for (int i = 0; i < block; i ++) dp[i][0] = minv[i];
        for (int j = 1; (1 << j) <= block; j ++) {
            for (int i = 0; i + (1 << j) - 1 < block; i ++) {
                int b1 = dp[i][j - 1], b2 = dp[i + (1 << (j - 1))][j - 1];
                dp[i][j] = a[b1] < a[b2]? b1 : b2;
            }
        }
    }
    int querymin(int a[], int L, int R) {
        int idl = L / blocklen, idr = R / blocklen;
        if (idl == idr)
            return idl * blocklen + f[s[idl]][L % blocklen][R % blocklen];
        else {
            int b1 = idl * blocklen + f[s[idl]][L % blocklen][blocklen - 1];
            int b2 = idr * blocklen + f[s[idr]][0][R % blocklen];
            int buf = a[b1] < a[b2]? b1 : b2;
            int c = t[idr - idl - 1];
            if (idr - idl - 1) {

                int b1 = dp[idl + 1][c];
                int b2 = dp[idr - 1 - (1 << c) + 1][c];
                int b = a[b1] < a[b2]? b1 : b2;
                return a[buf] < a[b]? buf : b;
            }
            return buf;
        }
    }
};

struct CartesianTree {
private:
    struct Node {
        int key, value, l, r;
        Node(int key, int value) {
            this->key = key;
            this->value = value;
            l = r = 0;
        }
        Node() {}
    };
    Node tree[maxn];
    int sz;
    int S[maxn], top;
    /** 小根堆 区间最小值*/
public:
    void build(int a[], int n) {
        top = 0;
        tree[0] = Node(-1, 0x80000000);//将根的key和value赋为最小以保持树根不变
        S[top ++] = 0;
        sz = 0;
        for (int i = 0; i < n; i ++) {
            tree[++ sz] = Node(i, a[i]);
            int last = 0;
            while (tree[S[top - 1]].value >= tree[sz].value) {
                last = S[top - 1];
                top --;
            }
            tree[sz].l = last;
            tree[S[top - 1]].r = sz;
            S[top ++] = sz;
        }
    }
    Node &operator [] (const int x) {
        return tree[x];
    }
};/** 树根为定值0,数组从0开始编号 **/

class stdRMQ {
public:
    void work(int a[], int n) {
        ct.build(a, n);
        dfs_clock = 0;
        dfs(0, 0);
        rmq.init(dfs_clock);
        rmq.initmin(depseq, dfs_clock);
    }
    int query(int L, int R) {
        int cl = clk[L], cr = clk[R];
        if (cl > cr) swap(cl, cr);
        return value[rmq.querymin(depseq, cl, cr)];
    }
private:
    CartesianTree ct;
    PlusMinusOneRMQ rmq;
    int dfs_clock, clk[maxn], value[maxn << 1], depseq[maxn << 1];
    void dfs(int rt, int d) {
        clk[ct[rt].key] = dfs_clock;
        depseq[dfs_clock] = d;
        value[dfs_clock ++] = ct[rt].value;
        if (ct[rt].l) {
            dfs(ct[rt].l, d + 1);
            depseq[dfs_clock] = d;
            value[dfs_clock ++] = ct[rt].value;
        }
        if (ct[rt].r) {
            dfs(ct[rt].r, d + 1);
            depseq[dfs_clock] = d;
            value[dfs_clock ++] = ct[rt].value;
        }
    }
};
时间: 2024-08-24 19:15:59

RMQ问题总结,标准RMQ算法的实现的相关文章

探讨排序算法的实现

排序算法是我们工作中使用最普遍的算法,常见的语言库中基本都会有排序算法的实现,比如c标准库的qsort,stl的sort函数等.本文首先介绍直接插入排序,归并排序,堆排序,快速排序和基数排序等比较排序算法,然后介绍计数排序,基数排序等具有线性时间的排序算法.本文主要讨论算法的实现方法,并不会过多介绍基本理论. 评价一个排序算法优劣适用与否,一般需要从三个方面来分析 时间复杂度.用比较操作和移动操作数的最高次项表示,由于在实际应用中最在乎的是运行时间的上限,所以一般取输入最坏情况的下的运行时间作为

Bug2算法的实现(RobotBASIC环境中仿真)

移动机器人智能的一个重要标志就是自主导航,而实现机器人自主导航有个基本要求--避障.之前简单介绍过Bug避障算法,但仅仅了解大致理论而不亲自动手实现一遍很难有深刻的印象,只能说似懂非懂.我不是天才,不能看几遍就理解理论中的奥妙,只能在别人大谈XX理论XX算法的时候,自己一个人苦逼的面对错误的程序问为什么... 下面开始动手来实现一下简单的Bug2避障算法.由于算法中涉及到机器人与外界环境的交互,因此需要选择一个仿真软件.常用的移动机器人仿真软件主要有Gazebo.V-rep.Webots.MRD

Canny边缘检测算法的实现

Canny原理 Canny的原理就不细说了,冈萨雷斯的<数字图像处理>(第三版)P463~465讲解的比较清楚,主要就四个步骤: 1. 对图像进行高斯滤波 2. 计算梯度大小和梯度方向 3. 对梯度幅值图像进行非极大抑制 4. 双阈值处理和连接性分析(通常这一步与非极大抑制并行,详见下面的代码) 下面重点说一下非极大抑制. 非极大抑制 对一幅图像计算梯度大小和梯度方向后,需要进行非极大抑制,一般都是通过计算梯度方向,沿着梯度方向,判断该像素点的梯度大小是否是极大值.这里主要说一下方向的判断.

STL简单&lt;stl_algorithms.h&gt;算法的实现

1.简介 STL标准中,没有区分基本算法和复杂算法,然而SGI STL却把常用的算法定义在<stl_algorithms.h>中.本文介绍部分<stl_algorithms.h>算法的实现,给出实现代码和测试代码. 本文介绍的算法包括: 1.      mismatch:比较两个序列,指出两者之间第一个不匹配的点,返回一对迭代器,分别指向两序列中不匹配的点: 2.      equal:如果两个序列在 [first, last ] 区间内相等,equal() 返回true,忽略第二

万年历算法的实现(C语言--gcc编译)

/** cal.c * * 现行的格里历是从儒略历演化而来的.儒略历每4年一个润年,润年366天,平年365天.* 如果从公元1年算的话,那么凡是能够被4整除的都是润年.从天文角度看,儒略历这种 * 历法是有误差的,到16世纪误差已经达到了10天.1582年,罗马教皇对儒略历进行了 * 一次校定,该年的10-5到10-14这10天被抹掉,并规定凡不能被400整除的世纪年不再 * 算为润年,校定之后的儒略历即为现行的格里历. * * 但是英国直到1752年才开始使用格里历,此时时间误差已经达到了1

搜索引擎--范例:中英文混杂分词算法的实现--正向最大匹配算法的原理和实现

纯中文和中英文混杂的唯一区别是,分词的时候你如何辨别一个字符是英文字符还是孩子字符, 人眼很容易区分,但是对于计算机来说就没那么容易了,只要能辨别出中文字符和英文的字符,分词本身就不是一个难题 1:文本的编码问题: utf8:windows下,以utf8格式保存的文本是一个3个字节(以16进制)的BOM的,并且你不知道一个汉字是否是用3位表示,但是英文适合ascii编码一样的 ascii:英文一位,中文两位,并且中文的第一个字节的值是大于128和,不会和英文混淆,推荐 unicode:中文基本是

软考笔记第六天之各排序算法的实现

对于前面的排序算法,用c#来实现 直接插入排序: 每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序.第一趟比较前两个数,然后把第二个数按大小插入到有序表中: 第二趟把第三个数据与前两个数从前向后扫描,把第三个数按大小插入到有序表中:依次进行下去,进行了(n-1)趟扫描以后就完成了整个排序过程.直接插入排序属于稳定的排序,最坏时间复杂性为O(n^2),空间复杂度为O(1).直接插入排序是由两层嵌套循环组成的.外层循环标识并决定待比较的数值.内层循环为待比较数值确定其最终位

图像旋转算法的实现

上一篇转载的文章(http://blog.csdn.net/carson2005/article/details/36900161)介绍了图像旋转的原理,这里给出代码实现,具体原理请参考上面的链接: 实现代码: void ImgRotate(cv::Mat imgIn, float theta, cv::Mat& imgOut) { int oldWidth = imgIn.cols; int oldHeight = imgIn.rows; // 源图四个角的坐标(以图像中心为坐标系原点) fl

Python 排序算法的实现

冒泡排序: 1 def bubble(l): 2 length = len(l) 3 for i in range(length): 4 for j in range(i+1, length): 5 if l[i] > l[j]: 6 l[i], l[j] = l[j], l[i] 7 print l 选择排序: 1 def select(l): 2 length = len(l) 3 for i in range(length): 4 minn = i 5 for j in range(i+1