ST表与二叉堆

本文深入探讨了ST表(稀疏表)和二叉堆这两种数据结构,并展示了它们在解决区间最值查询和优先级处理中的应用。通过实例分析,详细介绍了ST表的动态规划推导过程和优化成稀疏表的方法,以及二叉堆(大根堆和小根堆)在C++ STL中的实现与操作。同时,给出了相关编程题目的推荐,帮助读者巩固理解并提升实践能力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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 \ j12n-1n
11,11,21,n-11,n
2X2,22,n-12,n
n-1XXXn-1,n-1n-1,n
nXXXXn,n

注1,1代表从1到1,即f[1] [1],表格中以i, j形式给出两端点

给出关键两步:

1. 初始化及边界:

  1. 如果 i = j,f[i] [j] 即为 f[i] [i],从 i 到 i 就一个数,所以最值为本身

  2. 如果是求最小值,数组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][j1],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 / j012k
11,11,21,41, 2 k 2^k 2k
22,22,32,51, 2 k + 1 2^k+1 2k+1
33,33,43, 61, 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 模板及练习
  1. 模板
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;
}
  1. 练习

    洛谷:p3865ST模板、p2251质量检测

二、二叉堆

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,有些地方多加&,都有可能报错。记住,优先队列中主要有三种重载方式:

  1. 第一种:
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;
    }
};
  1. 第二种:
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;
    }
};
  1. 第三种:
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…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值