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 = ;
const static int M = ;
int blocklen, block, minv[N], dp[N][],
t[N * ], f[ << M][M][M], s[N];
void init(int n) {
blocklen = max(, (int)(log(n * 1.0) / log(2.0)) / );
block = n / blocklen + (n % blocklen > );
int total = << (blocklen - );
for (int i = ; i < total; i ++) {
for (int l = ; l < blocklen; l ++) {
f[i][l][l] = l;
int now = , minv = ;
for (int r = l + ; r < blocklen; r ++) {
f[i][l][r] = f[i][l][r - ];
if (( << (r - )) & i) now ++;
else {
now --;
if (now < minv) {
minv = now;
f[i][l][r] = r;
}
}
}
}
} int tot = N * ;
t[] = ;
for (int i = ; i < tot; i ++) {
t[i] = t[i - ];
if (!(i & (i - ))) t[i] ++;
}
}
void initmin(int a[], int n) {
for (int i = ; i < n; i ++) {
if (i % blocklen == ) {
minv[i / blocklen] = i;
s[i / blocklen] = ;
}
else {
if (a[i] < a[minv[i / blocklen]]) minv[i / blocklen] = i;
if (a[i] > a[i - ]) s[i / blocklen] |= << (i % blocklen - );
}
}
for (int i = ; i < block; i ++) dp[i][] = minv[i];
for (int j = ; ( << j) <= block; j ++) {
for (int i = ; i + ( << j) - < block; i ++) {
int b1 = dp[i][j - ], b2 = dp[i + ( << (j - ))][j - ];
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 - ];
int b2 = idr * blocklen + f[s[idr]][][R % blocklen];
int buf = a[b1] < a[b2]? b1 : b2;
int c = t[idr - idl - ];
if (idr - idl - ) { int b1 = dp[idl + ][c];
int b2 = dp[idr - - ( << c) + ][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 = ;
}
Node() {}
};
Node tree[maxn];
int sz;
int S[maxn], top;
/** 小根堆 区间最小值*/
public:
void build(int a[], int n) {
top = ;
tree[] = Node(-, 0x80000000);//将根的key和value赋为最小以保持树根不变
S[top ++] = ;
sz = ;
for (int i = ; i < n; i ++) {
tree[++ sz] = Node(i, a[i]);
int last = ;
while (tree[S[top - ]].value >= tree[sz].value) {
last = S[top - ];
top --;
}
tree[sz].l = last;
tree[S[top - ]].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 = ;
dfs(, );
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 << ], depseq[maxn << ];
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 + );
depseq[dfs_clock] = d;
value[dfs_clock ++] = ct[rt].value;
}
if (ct[rt].r) {
dfs(ct[rt].r, d + );
depseq[dfs_clock] = d;
value[dfs_clock ++] = ct[rt].value;
}
}
};

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

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

    移动机器人智能的一个重要标志就是自主导航,而实现机器人自主导航有个基本要求--避障.之前简单介绍过Bug避障算法,但仅仅了解大致理论而不亲自动手实现一遍很难有深刻的印象,只能说似懂非懂.我不是天才,不 ...

  2. Canny边缘检测算法的实现

    图像边缘信息主要集中在高频段,通常说图像锐化或检测边缘,实质就是高频滤波.我们知道微分运算是求信号的变化率,具有加强高频分量的作用.在空域运算中来说,对图像的锐化就是计算微分.由于数字图像的离散信号, ...

  3. C++基础代码--20余种数据结构和算法的实现

    C++基础代码--20余种数据结构和算法的实现 过年了,闲来无事,翻阅起以前写的代码,无意间找到了大学时写的一套C++工具集,主要是关于数据结构和算法.以及语言层面的工具类.过去好几年了,现在几乎已经 ...

  4. java基础解析系列(四)---LinkedHashMap的原理及LRU算法的实现

    java基础解析系列(四)---LinkedHashMap的原理及LRU算法的实现 java基础解析系列(一)---String.StringBuffer.StringBuilder java基础解析 ...

  5. SSE图像算法优化系列十三:超高速BoxBlur算法的实现和优化(Opencv的速度的五倍)

    在SSE图像算法优化系列五:超高速指数模糊算法的实现和优化(10000*10000在100ms左右实现) 一文中,我曾经说过优化后的ExpBlur比BoxBlur还要快,那个时候我比较的BoxBlur ...

  6. 详解Linux内核红黑树算法的实现

    转自:https://blog.csdn.net/npy_lp/article/details/7420689 内核源码:linux-2.6.38.8.tar.bz2 关于二叉查找树的概念请参考博文& ...

  7. 详细MATLAB 中BP神经网络算法的实现

    MATLAB 中BP神经网络算法的实现 BP神经网络算法提供了一种普遍并且实用的方法从样例中学习值为实数.离散值或者向量的函数,这里就简单介绍一下如何用MATLAB编程实现该算法. 具体步骤   这里 ...

  8. Python学习(三) 八大排序算法的实现(下)

    本文Python实现了插入排序.基数排序.希尔排序.冒泡排序.高速排序.直接选择排序.堆排序.归并排序的后面四种. 上篇:Python学习(三) 八大排序算法的实现(上) 1.高速排序 描写叙述 通过 ...

  9. Python八大算法的实现,插入排序、希尔排序、冒泡排序、快速排序、直接选择排序、堆排序、归并排序、基数排序。

    Python八大算法的实现,插入排序.希尔排序.冒泡排序.快速排序.直接选择排序.堆排序.归并排序.基数排序. 1.插入排序 描述 插入排序的基本操作就是将一个数据插入到已经排好序的有序数据中,从而得 ...

随机推荐

  1. D-Power Products

    题目连接: 题解: 根据题目的意思,对每个X进行质因子分解,保存其质因子以及质因子出现的个数,如果两个数的乘积变成一个数的K次幂,那么两个数的质因子的指数之间相加应为k的倍数.保存完毕后,开始遍历,将 ...

  2. Linux相关操作

    ssh配置秘钥 连接远程服务器时:需要用户持有“公钥/私钥对”,远程服务器持有公钥,本地持有私钥. 客户端向服务器发出请求.服务器收到请求之后,先在用户的主目录下找到该用户的公钥,然后对比用户发送过来 ...

  3. 解决项目迁移至Kubernetes集群中的代理问题

    解决项目迁移至Kubernetes集群中的代理问题 随着Kubernetes技术的日益成熟,越来越多的企业选择用Kubernetes集群来管理项目.新项目还好,可以选择合适的集群规模从零开始构建项目: ...

  4. shift后门

    shift快捷 Windows的粘滞键------C:\windows\system32\sethc.exe,它本是为不方便按组合键的人设计的 Windows系统按5下shift后,Windows就执 ...

  5. MySQL 查询语句优化思路

    query 语句的优化思路和原则主要提现在以下几个方面:1. 优化更需要优化的Query:2. 定位优化对象的性能瓶颈:3. 明确的优化目标:4. 从 Explain 入手:5. 多使用profile ...

  6. 为何 UNIX 时间 0, 有时显示是1970年1月1日,有时显示是1969年12月31日

    by Rachael Arnold http://www.rachaelarnold.com/dev/archive/why-is-date-returning-wrong Demystifying ...

  7. Oracle数据库字段保留3位小数,程序读出来显示4位小数

    需求 项目需求从字段2位小数,改成3位小数,这事儿好办,数据库噼里啪啦敲了一行代码,发现居然报错,原因是不能修改字段精度问题,然后使用了冒泡排序,搞定 --新增临时字段 ,); --将原字段内容拷贝至 ...

  8. 理解async/await

    async 和 await 在干什么 任意一个名称都是有意义的,先从字面意思来理解.async 是“异步”的简写,而 await 可以认为是 async wait 的简写.所以应该很好理解 async ...

  9. 【Linux网络基础】上网原理流程

    1. 局域网用户上网原理 上网过程说明: 确保物理设备和线路架构准备完毕,并且线路通讯状态良好 终端设备需要获取或配置上局域网(私有地址)地址,作为局域网网络标识 当终端设备想上网时,首先确认访问的地 ...

  10. vim的四种工作模式(转载别人的)

    Vim操作的四种模式 Vim的四种模式一.启动Vim1.双击桌面的图标,就可以启动Vim(是图形界面的)2.在开始菜单---点--运行 接着输入 vim 或者gvim,就可以启动Vim或Gvim了.二 ...