算法学习笔记(3.1): ST算法

算法,学习,笔记,st · 浏览次数 : 4

小编点评

**ST 表的倍增思想是如何体现的呢?** ST 表通过倍增思想,在预处理过程中选择一些长度为 \\(2\\) 的整数次幂的子区间作为代表值,并根据成倍增长的长度,计算出其最大值。 **ST 表不支持动态修改,如果需要动态修改,线段树是一种良好的解决方案,是 \\(O(n)\\) 的预处理时间复杂度,但是查询需要 \\(O(\log n)\\) 的时间复杂度那么ST表中倍增的思想是如何体现的呢?** 由于 ST 表不支持动态修改,如果需要动态修改,线段树是一种更好的选择,因为线段树可以根据问题的具体需求在初始化时进行调整。

正文

ST表

在RMQ(区间最值)问题中,著名的ST算法就是倍增的产物。ST算法可以在 \(O(n \log n)\) 的时间复杂度能预处理后,以 \(O(1)\) 的复杂度在线回答区间 [l, r] 内的最值。

当然,ST表不支持动态修改,如果需要动态修改,线段树是一种良好的解决方案,是 \(O(n)\) 的预处理时间复杂度,但是查询需要 \(O(\log n)\) 的时间复杂度

那么ST表中倍增的思想是如何体现的呢?

一个序列的子区间明显有 \(n^2\) 个,根据倍增的思想,我们在这么多个子区间中选择一些长度为 \(2\) 的整数次幂的区间作为代表值。

\(st[i][j]\) 表示子区间 \([i, i+2^j)\) 里最大的数

也可以表示为 \([i, i + 2^j -1 ]\),无论如何,其中有 \(2^j\) 个元素

下文中的 \(a\) 表示原序列

递推边界明显是 \(st[i][0] = a[i]\)

于是,根据成倍增长的长度,有了递推公式

\[st[i][j] = max(st[i][j-1],\;st[i+2^{j-1}][j-1]) \]

当询问任意区间 \([l, r]\) 的最值时,我们先计算出一个最大的 \(k\) 满足:\(2^k \le r - l + 1\),即需要不大于区间长度。那么,由于二进制划分我们可以知道,这个最大的 k 一定满足 \(2^{k+1}\ge r-l+1\),即我们只需要将两个长度为 \(2^k\) 的区间合并即可。

又根据 max(a, a) = a 可以知道,重复计算区间是没有任何问题的。

所以,在寻找最值的时候就有了以下公式:

\[max(a[l, r]) = max(st[l][k], st[r-2^k + 1][k]) \]

那么这里给出一种参考代码

// 啊,写这种预处理以2位底的对数的整数值的方式
// 我主要是为了将代码模块化,做到低耦合度
// 完全是可以分开来写的
class Log2Factory {
private:
    int lg2[N];
public:
    void init(int n) {
        for (int i = 2; i <= n; ++i) lg2[i] = lg2[i >> 1] + 1;
    }

    // 重载()运算符
    int operator() (const int &i) {
        return lg2[i];
    }
};

template<typename T>
class STable {
private:
    typedef T(*OP_FUNC)(T, T);

    Log2Factory Log2;
    T f[N][17]; // maybe most of the times k=17 is ok, make sure 2^k greater than N;
    OP_FUNC op;
public:
    void setOp(OP_FUNC fc) {
        op = fc;
    }

    void init(T *a, int n) {
        for (int i = 1; i <= n; ++i)
            f[i][0] = *(++a);

        int t = Log2(n);
        // f[i][k] is the interval of [i, i + 2^k - 1]
        // so f[i][k] can equal to the op sum of [i, i^k - 1]
        // let r = i^k - 1
        // => f[r - (1^k) + 1][k] can equal to the op sum of [i][k]
        for (int k = 1; k <= t; ++k) {
            for (int i = 1; i + (1<<k) - 1 <= n; ++i)
                f[i][k] = op(f[i][k-1], f[i + (1<<(k-1))][k-1]);
        }
    }

    const T query(int l, int r) {
        int k = Log2(r - l + 1);
        return op(f[l][k], f[r - (1<<k) + 1][k]);
    }
};

这……写法很神奇,注意修改!

扩展 - 运算

ST 算法不仅仅是可以求区间的最值的,只要是满足静态的,满足区间加法的问题大多数情况都可以通过 ST 表实现。

那么区间加法是什么意思呢?

定义我们需要对数列的筛选函数为 op ,则需要 op 满足以下性质

  • op(a, a) = a ,即重复参与运算不改变最终影响

  • op(a, b) = op(b, a) ,即满足交换律

  • op(a, op(b, c)) = op(op(a, b), c) ,即满足结合律

举个例子,如果我们求区间是否有负数,可以将 op 设为如下逻辑:

bool op(bool a, bool b) {
    return a | b;
}

相应的,初始化的方式也需要更改

if (a[i] < 0) st[i][0] = true;
else st[i][0] = false;

再举一个例子,如果我们需要求区间是否全为偶数时,则初始化为

if (a[i] % 2 == 0) st[i][0] = true;
else st[i][0] = false;

操作 op 定义为

bool op(bool a, bool b) {
    return a & b;
}

由此可见,其实ST算法可以做到的不仅仅是区间最值那么普通的东西啊。

但是,由于 加法 不满足性质一,所以,ST表通过这种方法并不能求得区间的所有满足某种性质的元素的个数。但是,通过另外一种 query 方式,我们可以做到这样。

扩展 - 区间

那么这个部分我们将讨论如何利用ST表做到上文例子中求区间偶数的个数。

同样,由于我们可以通过二进制划分,所以可以将某一个区间长度转化为多个长度为2的整数幂次方的子区间,并且可以保证这些区间不相互重叠。

所以我们可以利用这个处理 op(a, a) != a 的情况了。

其实这是借鉴了一点线段树的思路

虽然不如用线段树,但是胜在这常数极小 QwQ

那么可以写出以下代码

int query(int l, int r) {
    if (l == r) return st[l][0];
    int k = log2(r - l + 1);
    return op(st[l][k], query(l + (1<<k), r))
}

这样就满足了区间不重叠

或许会有一个问题,为什么初始化的时候不需要修改?

其实不难发现,初始化的合并是不会有重复贡献的情况的,即是每一次合并的区间是不会重叠的

与算法学习笔记(3.1): ST算法相似的内容:

算法学习笔记(3.1): ST算法

ST表 在RMQ(区间最值)问题中,著名的ST算法就是倍增的产物。ST算法可以在 \(O(n \log n)\) 的时间复杂度能预处理后,以 \(O(1)\) 的复杂度在线回答区间 [l, r] 内的最值。 当然,ST表不支持动态修改,如果需要动态修改,线段树是一种良好的解决方案,是 \(O(n)\

算法学习笔记(3): 倍增与ST算法

倍增 目录倍增查找 洛谷P2249重点变式练习快速幂ST表扩展 - 运算扩展 - 区间变式答案倍增更多的用法优化矩形查询优化建图优化 DP作者有话说 倍增,字面意思即”成倍增长“ 他与二分十分类似,都是基于”2“的划分思想 那么具体是怎么样,我们以一个例子来看 查找 洛谷P2249 依据题面,我们知

KD-Tree 学习笔记

学习资料: 1.B站 - 一只叫小花的猫 2.语雀 - 双愚:kdtree 3.B站视频:学习kdtree的前置知识:KNN算法 KD树简介与背景 k-d树,是一种分割k维数据空间的数据结构。主要应用于多维空间关键数据的搜索。关于kd树的背景,它主要是一种解决特征点匹配问题的算法,kd树就是一种高维

再谈23种设计模式(3):行为型模式(学习笔记)

行为型模式的关注点在于对象之间的通信和职责分配(描述结构模型中对象的动态特征)。行为型模式关注的是对象之间的交云和协作,即它们是如何相互作用的,以及如何分配职责和算法来完成任务。

VisionPro学习笔记(3)——BeadInspectTool

如果需要了解其他图像处理的文章,请移步小编的GitHub地址 传送门:请点击我 如果点击有误:https://github.com/LeBron-Jian/ComputerVisionPractice VisionPro有很多的示例和算子,这里再展示一个最新出的算子Bead Inspect Tool

谱图论:Laplacian二次型和Markov转移算子

以下部分是我学习CMU 15-751: TCS Toolkit的课堂笔记。接下来将要介绍的是谱图论(spectral graph theory)的关键,也就是Laplacian二次型(Laplacian quadratic form)。直观地理解,Laplacian二次型刻画了图的“能量”(ener...

算法学习笔记(6): 树链剖分

树链剖分 树链剖分是一个很神奇,但是在树上可以完成一些区间操作问题 简单来说,就是把一棵树分成一条条的链,通过维护链上的信息来维护整棵树的信息 基础知识可以参考我的另外一篇博客:算法学习笔记(5): 最近公共祖先(LCA) 这里假设你已经掌握了上述博客中的所有相关知识,并清晰了其背后的原理 性质?发

算法学习笔记(11): 原根

原根 此文相对困难,请读者酌情食用 在定义原根之前,我们先定义其他的一点东西 阶 通俗一点来说,对于 $a$ 在模 $p$ 意义下的阶就是 $a^x \equiv 1 \pmod p$ 的最小正整数解 $x$ 或者说,$a$ 在模 $p$ 意义下生成子群的阶(群的大小) 再或者说,是 $a$ 在模

算法学习笔记(30):Kruskal 重构树

Kruskal 重构树 这是一种用于处理与最大/最小边权相关的一个数据结构。 其与 kruskal 做最小生成树的过程是类似的,我们考虑其过程: 按边权排序,利用并查集维护连通性,进行合并。 如果我们在合并时,新建一个节点,其权值为当前处理的边的权值,并将合并的两个节点都连向新建的节点,那么就可以得

C++算法之旅、09 力扣篇 | 常见面试笔试题(上)算法小白专用

算法学习笔记,记录容易忘记的知识点和难题。详解时空复杂度、50道常见面试笔试题,包括数组、单链表、栈、队列、字符串、哈希表、二叉树、递归、迭代、分治类型题目,均带思路与C++题解