ST表与二叉堆
文章目录
一、ST表
1.1 ST表的简介
st表,即sparse table,中文名“稀疏表”。它的作用是:解决静态RMQ(Range Min/Max Query)问题,即区间最值的查询。举个例子:
例:给你一个有n个数的数组(n ≤ \leq ≤ 100000),查询m(m ≤ \leq ≤ 100000)次,每次查询给出左、右端点,要求输出该(左右端点)范围内的最大或者最小值
暴力做法:对于每一次查询,进行一次遍历。
那么时间复杂度即为O(n * m)的,很明显不适用于n在100000的数量级。
这时候,对于仅仅是查询而言,st表就能发挥巨大的优势,如果涉及修改某一值的情况,这就显然不是st的范畴了,需要用树状数组或者线段树来解决
1.2 ST表的推导
1.2.1 纯暴力
这没什么好说的,因为数组中的数字是无序的,所以每次查询都需要依次遍历来寻找。
假设有查询函数int find(int l, int r);
该函数返回a[l, r]区间的最大值
则代码如下:
int m, l, r;
int find(int l, int r)
{
int mx = -INF;
for (int i = l; i <= r; ++ i) mx = max(mx, a[i]);
return mx;
}
while (m --)//m次查询
{
cin >> l >> r;//每次查询给出区间范围
cout << find(l, r) << '\n';//输出区间范围内的最值
}
TLE(超时)是一定的…
1.2.2 动态规划(Dynamic Programming)
这时候,我们想到用动态规划解决这个问题。
二维数组f[i] [j]表示从第i个数到底j个数这个区间的最大值
用表格展示一下:
i \ j | 1 | 2 | … | n-1 | n |
---|---|---|---|---|---|
1 | 1,1 | 1,2 | … | 1,n-1 | 1,n |
2 | X | 2,2 | … | 2,n-1 | 2,n |
… | … | … | … | … | … |
n-1 | X | X | X | n-1,n-1 | n-1,n |
n | X | X | X | X | n,n |
注1,1代表从1到1,即f[1] [1],表格中以i, j形式给出两端点
给出关键两步:
1. 初始化及边界:
-
如果 i = j,f[i] [j] 即为 f[i] [i],从 i 到 i 就一个数,所以最值为本身
-
如果是求最小值,数组a下标从1开始,那么边界a[0]初始化为 ∞ \infty ∞
反之求最大值,边界a[0]初始化为- ∞ \infty ∞。
2. 状态转移方程: (以最大值为例)
f
[
i
]
[
j
]
=
{
a
[
i
]
;
i
=
j
m
a
x
(
f
[
i
]
[
j
−
1
]
,
a
[
j
]
)
;
i
<
j
0
;
i
>
j
/
/
不
需
要
f[i][j] = \left\{ \begin{aligned} & a[i];\ i = j\\ & max(f[i][j-1],a[j]); i < j \\ & 0; i > j//不需要 \end{aligned} \right.
f[i][j]=⎩⎪⎨⎪⎧a[i]; i=jmax(f[i][j−1],a[j]);i<j0;i>j//不需要
但是,这个是O(n * n)的,而且二维数组f[N] [N],太大了根本开不出或者很难开出,大概几百MB。
但是,这个虽然不行,但是给了我们新的思考方向…
1.2.3 稀疏表(Sparse Table)
由于任意一个正整数x都可以表示为:
x = 2 a 0 + 2 a 1 + 2 a 2 + . . . + 2 a k x = 2^{a0} + 2^{a1} + 2^{a2} + ... + 2^{ak} x=2a0+2a1+2a2+...+2ak
所以,我们重新定义f[i] [j],新的f[i] [j]数组表示
f[i] [j] 表示从 i 开始,长度为 2 j 2^j 2j的区间最大值。
这个区间为 [i, i + 2^j - 1]
再次用表格展示:
i / j | 0 | 1 | 2 | … | k |
---|---|---|---|---|---|
1 | 1,1 | 1,2 | 1,4 | … | 1, 2 k 2^k 2k |
2 | 2,2 | 2,3 | 2,5 | … | 1, 2 k + 1 2^k+1 2k+1 |
3 | 3,3 | 3,4 | 3, 6 | … | 1, 2 k + 2 2^k+2 2k+2 |
… | … | … | … | … | … |
n- 2 j 2^j 2j+1 | |||||
… |
思路:初始化长度为1的区间,从区间长度为2开始枚举区间长度
这与区间DP有相似之处
另外,因为用到二进制,就把原来的:f[i][j], i <= 100000, j <= 100000
变成:f[i][j], i <= 100000, j <= 17
因为 2 16 2^{16} 216为65536,不到100000,所有最小取17
二维数组变成f[N][M]
,这里由于M = 17,也就是说这个二维数组大小不超过2000000(2e6)int的大小,空间上完全可以。
f[i] [j]数组经过一系列的处理,就变成了ST表
i 从 1 ~ N 执行 N次
j 从 2^0 ~ 2^k(i + 2^k - 1<= N) 由于是指数倍增,所以执行 l o g 2 N log_2N log2N次
总时间复杂度为O(nlogn),也可看成O(n * m),其实这个m就等于 l o g 2 N log_2N log2N。
如何按照所给的查询区间l, r来对应的查ST表?
答:给定任意区间[l, r]都有,len = r - l + 1;
假设len的长度为 :[——————————]
核心:我们需要算出最大的、2的指数长度的、能将区间完全覆盖的两段
这里不加证明地给出,每一段长度 k = log2len; 有
len[-——————————]
k [——————]
k [———————]
因为2的指数长度的每一段我们都算了一遍,所以st表中存了这两段的最值,所以,将这两段最值再取一个最值即可得到[l, r]区间的最值,
即max{a[l]~a[ r]} = max(f[l] [k], f[r-( 2 k 2^k 2k)+1] [k]), 其中第二项f[r-( 2 k 2^k 2k)+1] [k],r- 2 k 2^k 2k+1是从末尾r往前数 2 k 2^k 2k位得到的起点.
1.3 模板及练习
- 模板
const int N = 100010;
int f[N][17];
int a[N], n, m;//n个数,存于a[N]中,m个询问
void ST_init()
{
for (int i = 1; i <= n; ++ i;) f[i][0] = a[i];
for (int j = 1; (1<<j) <= n; ++ j)
{
for (int i = 1; i + (1<<j) - 1 <= n; ++ i)
f[i][j] = max(f[i][j-1], f[i+(1<<(j-1))][j-1]);
}
return;
}
int find_ST(int l, int r)
{
int k = log(r-l+1)/log2;
return max(f[l][k], f[r-(1<<k)+1][k]);
}
int main()
{
cin >> n;
for (int i = 1; i <= n; ++ i) cin >> a[i];
ST_init();
int l, r;
while (n --)
{
cin >> l >> r;
cout << find_ST(l, r) << '\n';
}
system("pause");
return 0;
}
二、二叉堆
2.1 二叉堆的简介
二叉堆,别的不用了解,知道有大根堆和小根堆就行了。
大根堆又称最大堆:指所有双亲节点均大于其左右孩子的二叉树结构
小根堆又称最小堆:指所有双亲节点均小于其左右孩子的二叉树结构
了解这么多就行了,这里不介绍用数组来模拟堆的结构,而是介绍STL库中一种与二叉堆相关的数据结构,优先队列
两种优先队列:
#include <queue>//头文件包含
小根堆:内部数据可看成从小到大排列
priority_queue <int, vector<int>, greater<int> > heap;
//定义了一个名为heap的小根堆,堆顶元素为最小值
大根堆:内部数据可看成从大到小排列
//因为默认就是大根堆,所以
priority_queue<int, vector<int> > heap//就定义了heap大根堆
//当然也可以仿照小根堆的写法
priority_queue <int, vector<int>, less<int> > heap;
//等同大根堆
优先队列支持的操作:
priority_queue <int, vector<int>, less<int> > heap;
//对于优先队列(从大到小)heap来说,
int x;
1. 进队: heap.push(x);
2. 出队: heap.pop();
3. 队头(即堆顶)元素的访问: heap.top();
4. 大小: heap.size();
5. 是否为空: heap.empty()//返回bool值表示真假
对于自己定义的结构体类型,要注意对大小关系判断符’<'进行重载
可能与直接定义的结构体或类有细微不同,有些地方不加const,有些地方多加&,都有可能报错。记住,优先队列中主要有三种重载方式:
- 第一种:
struct node
{
int a, b, c;
bool operator< (const node& p)const
{
if (a != p.a)//第一优先级为a
return a < p.a;//按小到大排
if (b != p.b)//第二优先级为b
return b < p.b;//按小到大排
return c < p.c;
}
};
- 第二种:
struct node
{
int a, b, c;
friend bool operator< (node p, node q)
{//c语言括号里要写成struc node a, struct node b,这里是c++
if (p.a != q.a)//第一优先级为a
return p.a < q.a;//按小到大排
if (p.b != q.b)//第二优先级为b
return p.b < q.b;//按小到大排
return p.c < p.c;
}
};
- 第三种:
struct node
{
int a, b, c;
};
bool operator< (node p, node q)
{
if (p.a != q.a)//第一优先级为a
return p.a < q.a;//按小到大排
if (p.b != q.b)//第二优先级为b
return p.b < q.b;//按小到大排
return p.c < p.c;
}
在优先队列中,重载时,如果想从小到大排,呈小根堆,那么
不是return p.a < q.a
或者return p.b < q.b
,而是相反的
return p.a > q.a
,或者return p.b < q.b
简而言之,符号得反过来。
不解释。
2.2 用二叉堆解决问题
洛谷:p1168中位数、p2085最小函数值、p1631序列合并、p1878舞蹈课
题解都有,我就不写了。这些题目可以拿过来练习一下,题不在多,精做,多思考。
THE END…