这篇文章上次修改于 481 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

算法学习笔记(6): Sparse Table (ST 表)

简介

ST 表 是一个数据结构,用于计算RMQ问题,其思想基于动态规划

RMQ问题

RMQ即Range Minimum / Maximum Query,区间的最大值最小值查询,即求区间最值的问题。

内容:

给含有n个元素的数组a,有多次查询,每次查询有变量L,R,问区间[L, R] 中的最大/小值。

分析:

解法1

对于区间L,R,只要遍历它们就可以找到最大值,这也是最暴力的解法,其时间复杂度为O(N2).

对于暴力解法,需要消耗大量的时间,多次查询都是一次新的查询,不利于降低时间复杂度。

解法2

因为有多次查找,不如创建数组d[n][n],其中d[i][j] 表示区间i j中的最大值。其中我们有:

d[i][i] = a[i]
d[i][j] = max(d[i][j - 1], a[j])

对此,我们可以得到所有的查询的值,但时间复杂度并没有很大的下降,同时,空间复杂度提升到了O(N2)

对于这个d,我们将其称为,我们可以发现,该表存储的多个区间高度重合,因此,该表的密度很高。

虽然解法2使用了DP的思想,但是仍有很大的进步空间。

ST表(稀疏表)

对于我们解法二,提到了该表的多个区间重合度问题,对于这一点,我们可以考虑能否降低区间重合度。

定义

ST表提出储存d[i][j]代表区间[i, i + 2^j) 左闭右开中的最值。

也可以储存最值的下标

查询

我们先假设该表已经完成预处理,该表的值已经储存完毕。

对于查询区间[x, y],我们可知想其长度为y - x + 1, 对此,它必定能由两个区间并集产生。

两个区间特点:长度为2的幂值,两个区间可能重合,例如对于区间 [0, 13]

可得两个区间为 [0, 7] 和 [6, 13]。易得这两个区间的最大值也就是d[0][3]和d[6][3]

两个区间的最大值也就是这两个区间并集区间的最大值。接下来考虑如何获得这两个区间。

len = r - l + 1
i = log2(len) // 向下取整
result = max(d[l][i], d[r - (1 << i) + 1][i])

预处理

d[i][0] = i;

d[i][j] = max(d[i][j - 1], d[i + (1 << (j - 1))][j - 1]);

void init(const vector<int> & arr) {
    for (int j = 0; j < arr.size(); j++) {
        for (int i = 1; i + (1 << (j - 1)) <= arr.size(); j++) {
			if(j == 0) d[i][j] = i;
            else{
				d[i][j] = max(f[i][j - 1], d[i + 1 << (j - 1)][j - 1]);
            }
        }
    }
}

同样使用了DP的思路,对于区间[0, 7] (d[0, 3])已知区间[0, 3] [4, 7] 分别是 d[0, 2], d[4, 2]

转化之后即为

d[0, 3] = max(d[0, 2], d[4, 2]);

实现的稀疏表类

class SparseTable {
public:
    SparseTable(const vector<int>& array) {
        this->array = array;
        this->n = array.size();
        this->k = log2(n) + 1;
        this->table.resize(n, vector<int>(k));

        buildTable();
    }

    int query(int left, int right) {
        int j = log2(right - left + 1);
        if (array[table[left][j]] <= array[table[right - (1 << j) + 1][j]]) {
            return table[left][j];
        } else {
            return table[right - (1 << j) + 1][j];
        }
    }

private:
    void buildTable() {
        for (int i = 0; i < n; ++i) {
            table[i][0] = i;
        }
        for (int j = 1; (1 << j) <= n; ++j) {
            for (int i = 0; i + (1 << j) - 1 < n; ++i) {
                if (array[table[i][j - 1]] <= array[table[i + (1 << (j - 1))][j - 1]]) {
                    table[i][j] = table[i][j - 1];
                } else {
                    table[i][j] = table[i + (1 << (j - 1))][j - 1];
                }
            }
        }
    }

    vector<int> array;
    vector<vector<int>> table;
    int n, k;
};

RMQ二维扩展

简略说明一下

d[a][b][c][d] 为 [(a, b), (c + 2 ^ a - 1, d + 2 ^ b - 1)]

按照同样思路完成即可。

部分应用例题

例题1:区间GCD

给定一个数字序列,多次询问[l, r]区间内所有数的最大公因数。

我们易发现gcd的连续性,和取最大值类似,故直接替换函数即可。

参考