数据结构——ST表和RMQ问题

数据结构------ST表和RMQ问题

  • 数据结构ST表和RMQ问题
  • ST表的应用
    • [P3865 【模板】ST 表 & RMQ 问题 - 洛谷](#P3865 【模板】ST 表 & RMQ 问题 - 洛谷)
    • [P1890 gcd 区间 - 洛谷](#P1890 gcd 区间 - 洛谷)
    • [P2251 质量检测 - 洛谷](#P2251 质量检测 - 洛谷)
    • [P2880 Balanced Lineup G - 洛谷](#P2880 Balanced Lineup G - 洛谷)
    • [P1198 最大数 - 洛谷](#P1198 最大数 - 洛谷)
    • [P7809 01 序列 - 洛谷](#P7809 01 序列 - 洛谷)
  • [OJ 参考](#OJ 参考)

数据结构ST表和RMQ问题

RMQ 问题:区间内最大、最小值查询(Range Minimum/Maximum Query)。对于一个长度为 n n n 的序列,有 m m m 次查询操作,每次查询为一个区间 l , r l, r l,r 的最大值或最小值。

RMQ 问题可以用线段树 解决。对于这种只有查询操作没有修改操作的静态问题,还可以用代码量更少的 ST 表来解决。

ST 表(Sparse Table,稀疏表)也可以称呼为 ST 算法,是基于动态规划 ( 区间dp ) 和倍增 实现的数据结构,形式上是一张二维表格。ST 表通过预处理维护一些区间信息,从而快速处理区间查询。

类似前缀和数组。

其中预处理的时间复杂度为 O ( n log ⁡ n ) \text{O}(n \log n) O(nlogn),查询操作为 O ( 1 ) \text{O}(1) O(1)。由于在查询前需要预处理,ST 表基本上只能解决静态问题

ST 表维护的信息需要满足结合律 以及可重复贡献 。可重复贡献是某个操作, 2 个相同的数执行这个操作得到的结果还是这个数,例如区间最值 以及区间最大公约数就是可重复贡献的问题。如果不满足结合律以及可重复贡献,ST 表就不能解决,例如区间和以及区间乘积。

ST表维护信息的方式

ST 表常用于解决 RMQ 问题:对于一个长度为 n n n 的序列,有 m m m 次查询操作,每次查询为一个区间 l , r l, r l,r 的最大值。

由于区间最值不满足可差性,因此不能像前缀和数组一样,搞一张一维的表格来预处理某些区间的信息,尽管可以使用线段树结局,但同样希望能有更加轻量级的数据结构。

可以尝试用区间 dp 的 dp 表来预处理。

由于二维表格可以直接用来表示区间,那么一种直接的方式就是:使用区间 dp , d p i j dpij dpij 表示区间 i , j i, j i,j 的最值。

这种方式肯定是可以解决问题的。但是 RMQ 问题的数组一般都是 10 5 ∼ 10 6 10^5 \sim 10^6 105∼106 级别的长度,这张二维表压根创建不出来,更不用说潜在的遍历超时问题。

ST 表尝试用 2 j = 2 j − 1 + 2 j − 1 2^j = 2^{j-1} + 2^{j-1} 2j=2j−1+2j−1 优化区间 dp 的状态表示:
d p i j dpij dpij 代表的含义为:从 i i i 位置开始,长度为 2 j 2^j 2j 的区间中,所有元素的最值。

此时空间复杂度可压缩到 O ( n log ⁡ n ) \text{O}(n\log n) O(nlogn) ,可以容纳 10 6 10^6 106 的数组,且求最值时可从 2 个长度为 2 j − 1 2^{j-1} 2j−1 的空间中求解(或进行状态转移)。

以数组 a = 5 , 2 , 4 , 6 , 1 , 7 , 5 , 0 , 9 , 3 a = 5, 2, 4, 6, 1, 7, 5, 0, 9, 3 a=5,2,4,6,1,7,5,0,9,3 为例,我们会用下述方式维护区间最大值信息:
开始时没有处理: 5 2 4 6 1 7 5 0 9 3 1 2 3 4 5 6 7 8 9 10 第0列长度为1: 5 2 4 6 1 7 5 0 9 3 第1列长度为 2: 5 4 6 6 7 7 5 9 9 第2列长度为 4: 6 6 7 7 7 9 9 第3列长度为8: 7 9 9 \begin{matrix} \text{开始时没有处理:} &\begin{array}{|c|c|}\hline5&2&4&6&1&7&5&0&9&3\\\hline1&2&3&4&5&6&7&8&9&10\\\hline\end{array}\\ \text{第0列长度为1:}&\begin{array}{|c|c|}\hline5&2&4&6&1&7&5&0&9&3\\\hline\end{array}\\ \text{第1列长度为 2:}&\begin{array}{|c|c|}\hline5&4&6&6&7&7&5&9&9&\ \ \\\hline\end{array}\\ \text{第2列长度为 4:}&\begin{array}{|c|c|}\hline6&6&7&7&7&9&9&\ \ &\ \ &\ \ \\\hline\end{array}\\ \text{第3列长度为8:}&\begin{array}{|c|c|}\hline7&9&9&\ \ &\ \ &\ \ &\ \ &\ \ &\ \ &\ \ \\\hline\end{array} \end{matrix} 开始时没有处理:第0列长度为1:第1列长度为 2:第2列长度为 4:第3列长度为8:5122436415765708993105246175093546677599 6677799 799

维护方式是在每个长度为 2 i 2^i 2i 的区间内求最值。

这就是稀疏表的由来,并不是把所有的区间信息存下来,这是暴力算法做的事, ST 表只保存长度为 2 j 2^j 2j 的区间信息。

ST 表的查询

对于每次查询 l , r l, r l,r,可以把它分成两个区间 l , l + 2 k − 1 l, l + 2\^k - 1 l,l+2k−1 r − 2 k + 1 , r r - 2\^k + 1, r r−2k+1,r,其中 k = ⌊ log ⁡ 2 ( r − l + 1 ) ⌋ ≤ r − l + 1 k = \lfloor\log_2(r - l + 1)\rfloor \leq r-l+1 k=⌊log2(r−l+1)⌋≤r−l+1 ,查询的结果就是这两个区间最大值的最大值,重叠部分并不影响。

在预处理的 d p dp dp 数组中,拿到 d p l k dplk dplk 和 d p r − ( 1 \< \< k ) + 1 k dpr - (1 \<\< k) + 1k dpr−(1\<\k 两个格子,取最大值即可,即 max l , r = max ( d p l , k , d p r − ( 1 \< \< k ) + 1 k ) \text{max}_{l,r}=\text{max}(dpl,k,dpr - (1 \<\< k) + 1k) maxl,r=max(dpl,k,dpr−(1\<\k) 。

记忆区间起点和终点的技巧:

  • 起点 + + + 区间长度 = = = 下一个区间的起点。即 l + l e n l+len l+len 即为第 2 个区间的左端点。
  • 终点 − - − 区间长度 = = = 上一个区间的终点。即 r − l e n r-len r−len 即为第 1 个区间的右端点。

ST 表的实现 - 预处理

可以用动态规划的方式思考:

  1. 状态表示: d p i j dpij dpij 表示:从 i i i 位置开始,长度为 2 j 2^j 2j 的区间中,所有元素的最大值。

  2. 状态转移方程:

    因为 2 j − 1 + 2 j − 1 = 2 j 2^{j-1} + 2^{j-1} = 2^j 2j−1+2j−1=2j ,所以长度为 2 j 2^j 2j 的区间,可以分成 2 个长度为 2 j − 1 2^{j-1} 2j−1 的区间。

因此, d p i j = max ⁡ ( d p i j − 1 , d p i + ( 1 \< \< ( j − 1 ) ) j − 1 ) dpij = \max(dpij-1, dpi+(1\<\<(j-1))j-1) dpij=max(dpij−1,dpi+(1\<\<(j−1))j−1) 。这里使用位运算而不使用快速幂的原因是快速幂的时间复杂度是 O ( log ⁡ j ) \text{O}(\log j) O(logj) ,而位运算在数据量不超过范围的情况下是 O ( 1 ) \text{O}(1) O(1) 。

  1. 初始化:

区间长度为 2 0 = 1 2^0 = 1 20=1 时,最大值就是数组本身,因此可以把第 0 列初始化为原始数组。

  1. 填表顺序:

通过小区间转移到大区间。因此第一层循环从小到大枚举 j j j,第二层循环从小到大枚举起点。

注意两个边界:

  1. 对于 j j j :枚举的区间长度不能超过 n n n,因此 j j j 的最大值应该为 log ⁡ 2 n \log_2 n log2n。
  2. 对于 i i i :当区间长度为 2 j 2^j 2j 时,最后一个区间的右端点不能超过 n n n,因此 i + ( 1 < < j ) − 1 ≤ n i + (1 << j) - 1 \leq n i+(1<<j)−1≤n。

优化 :若查询次数过多,求对数时是会有一个 log ⁡ \log log 级别的开销的。若把 log ⁡ 2 1 ∼ log ⁡ 2 n \log_2 1 \sim \log_2 n log21∼log2n 全部预处理出来,则查询操作的 k k k 就可以在 O ( 1 ) O(1) O(1) 时间得到。

对于 log ⁡ 2 i \log_2 i log2i,容易得到一个关系式:

log ⁡ 2 i = log ⁡ 2 ( i 2 × 2 ) = log ⁡ 2 i 2 + 1 \log_2 i = \log_2 \left( \frac{i}{2} \times 2 \right) = \log_2 \frac{i}{2} + 1 log2i=log2(2i×2)=log22i+1

其中 log ⁡ 2 1 = 0 \log_2 1 = 0 log21=0,因此可以通过递推,预处理出来所有的 log ⁡ 1 ∼ log ⁡ n \log 1 \sim \log n log1∼logn。

ST 表参考(封装):

cpp 复制代码
using vi = vector<int>;
using vvi = vector<vector<int>>;
struct ST {
    vi lg2; // 2的若干次幂
    vvi dp; // ST表本体
    // 要求 log2(a.size()-1)<dp[0].size()
    ST(const vi &a = vi()) {
        if (a.size())
            init(a);
    }
    void init(const vi &a) {
        lg2.resize(a.size());
        dp.resize(a.size(), vi(log2(a.size() - 1) + 1, 0));
        // 原本对数函数的定义域是(0,正无穷),这里是方便初始化lg2[1]
        lg2[0] = -1;
        for (int i = 1; i < a.size(); i++) {
            lg2[i] = lg2[i >> 1] + 1;
            dp[i][0] = a[i];
        }
        // 区间dp
        for (int j = 1; j <= lg2[a.size() - 1]; j++) // 枚举区间长
            for (int i = 1; i + (1 << j) - 1 < a.size(); i++) // 右端点不越界
                dp[i][j] = max(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);
    }
    int query(int l, int r) {
        int k = lg2[r - l + 1];
        return max(dp[l][k], dp[r - (1 << k) + 1][k]);
    }
};

ST表的应用

P3865 【模板】ST 表 & RMQ 问题 - 洛谷

P3865 【模板】ST 表 & RMQ 问题 - 洛谷

ST 表模板题,也可当成区间 dp 的题来分析,但也只是分析,不可能开 2 个维度都是 10 5 10^5 105 大小的数组。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using vi = vector<int>;
using vvi = vector<vector<int>>;
struct ST {
    vi lg2; // 2的若干次幂
    vvi dp; // ST表本体
    // 要求 log2(a.size()-1)<dp[0].size()
    ST(const vi &a = vi()) {
        if (a.size())
            init(a);
    }
    void init(const vi &a) {
        lg2.resize(a.size());
        dp.resize(a.size(), vi(log2(a.size() - 1) + 1, 0));
        // 原本对数函数的定义域是(0,正无穷),这里是方便初始化lg2[1]
        lg2[0] = -1;
        for (int i = 1; i < a.size(); i++) {
            lg2[i] = lg2[i >> 1] + 1;
            dp[i][0] = a[i];
        }
        // 区间dp
        for (int j = 1; j <= lg2[a.size() - 1]; j++)          // 枚举区间长
            for (int i = 1; i + (1 << j) - 1 < a.size(); i++) // 右端点不越界
                dp[i][j] = max(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);
    }
    int query(int l, int r) {
        int k = lg2[r - l + 1];
        return max(dp[l][k], dp[r - (1 << k) + 1][k]);
    }
};

void IOinit() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
}

int main() {
    // freopen("in.in", "r", stdin);
    IOinit(); // 没它过不了OJ
    int n, m;
    cin >> n >> m;
    vi a(n + 1, 0);
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    ST st(a);
    for (int i = 1; i <= m; i++) {
        int l, r;
        cin >> l >> r;
        cout << st.query(l, r) << '\n';
    }
    return 0;
}

P1890 gcd 区间 - 洛谷

P1890 gcd 区间 - 洛谷

ST表解法

gcd 满足结合率: gcd ( a , b ) = gcd ( b , a ) \text{gcd}(a,b)=\text{gcd}(b,a) gcd(a,b)=gcd(b,a) ,同时也满足可重复贡献,所以可以用 ST 表存储,但初始化和查询的运算方式要从最值变成求 gcd 。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using vi = vector<int>;
using vvi = vector<vector<int>>;

void IOinit() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
}

struct ST {
    vi lg2;
    vvi dp;
    ST(const vi &a = vi()) {
        if (a.size())
            init(a);
    }
    void init(const vi &a) {
        lg2.resize(a.size(), 0);
        dp.resize(a.size(), vi(log2(a.size() - 1) + 1, 0));
        lg2[0] = -1;
        for (int i = 1; i < a.size(); i++) {
            lg2[i] = lg2[i >> 1] + 1;
            dp[i][0] = a[i];
        }
        for (int j = 1; j <= lg2[a.size() - 1]; j++)
            for (int i = 1; i + (1 << j) - 1 < a.size(); i++)
                dp[i][j] = gcd(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);
    }
    int gcd(int a, int b) {
        return b ? gcd(b, a % b) : a;
    }
    int query(int l, int r) {
        int k = lg2[r - l + 1];
        return gcd(dp[l][k], dp[r - (1 << k) + 1][k]);
    }
};

int main() {
    // freopen("in.in", "r", stdin);
    IOinit();
    int n, m;
    cin >> n >> m;
    vi a(n + 1, 0);
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    ST st(a);
    for (int i = 1; i <= m; i++) {
        int l, r;
        cin >> l >> r;
        cout << st.query(l, r) << '\n';
    }
    return 0;
}

区间dp解法

注意到这个题的初始序列长度只有 1000 ,每个数的最大值可达 10 9 10^9 109 ,算上使用欧几里得算法带来的额外耗时和 -O2 优化,勉强可用基于区间上的某个分界点的区间 dp 解决,转移方程为

d p i j = gcd ( d p i k , d p k + 1 j ) dpij=\text{gcd}(dpik,dpk+1j) dpij=gcd(dpik,dpk+1j) ,分界点 k = i + j 2 k=\frac{i+j}{2} k=2i+j 。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using vi = vector<int>;
using vvi = vector<vi>;

int gcd(int a, int b) {
    return b ? gcd(b, a % b) : a;
}

int main() {
    // freopen("in.in", "r", stdin);
    vvi dp;
    int n, m;
    cin >> n >> m;
    dp.resize(n + 1, vi(n + 1, 0));
    for (int i = 1; i <= n; i++)
        cin >> dp[i][i];
    // 区间dp
    for (int len = 2; len <= n; len++)
        for (int i = 1; i + len - 1 <= n; i++) {
            int j = i + len - 1;
            int k = (i + j) / 2; // 求gcd,分界点只需要1个
            dp[i][j] = gcd(dp[i][k], dp[k + 1][j]);
        }
    for (int i = 1; i <= m; i++) {
        int l, r;
        cin >> l >> r;
        cout << dp[l][r] << '\n';
    }
    return 0;
}

P2251 质量检测 - 洛谷

P2251 质量检测 - 洛谷

此题为单调队列模板题。核心依旧是 RMQ 问题,这里使用 ST 表解决。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using vi = vector<int>;
using vvi = vector<vector<int>>;

void IOinit() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
}

struct ST {
    vi lg2;
    vvi dp;
    ST(const vi &a = vi()) {
        if (!a.empty())
            init(a);
    }
    void init(const vi &a) {
        lg2.resize(a.size(), 0);
        dp.resize(a.size(), vi(log2(a.size() - 1) + 1, 0));
        lg2[0] = -1;
        for (int i = 1; i < a.size(); i++) {
            lg2[i] = lg2[i / 2] + 1;
            dp[i][0] = a[i];
        }
        for (int j = 1; j <= lg2[a.size() - 1]; j++)
            for (int i = 1; i + (1 << j) - 1 < a.size(); i++)
                dp[i][j] = min(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);
    }
    int query(int l, int r) {
        int k = lg2[r - l + 1];
        return min(dp[l][k], dp[r - (1 << k) + 1][k]);
    }
};

int main() {
    // freopen("in.in", "r", stdin);
    int n, m;
    cin >> n >> m;
    vi a(n + 1, 0);
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    ST st(a);
    for (int i = 1; i + m - 1 <= n; i++)
        cout << st.query(i, i + m - 1) << '\n';
    return 0;
}

P2880 Balanced Lineup G - 洛谷

P2880 [USACO07JAN Balanced Lineup G - 洛谷](https://www.luogu.com.cn/problem/P2880)

区间查询时要求同时获得区间最大值和区间最小值,可通过 2 个 ST 表进行维护。这里用函数指针数组丰富 ST 表的功能。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using vi = vector<int>;
using vvi = vector<vector<int>>;

void IOinit() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
}

struct ST {
    vi lg2;
    vvi dp;
    vector<int (*)(int &, int &)> ope; // 函数指针数组
    int op;
    ST(const vi &a = vi(), int _op = 0) {
        ope = {mmax, mmin}; // c++11的初始化列表
        op = _op;
        if (!a.empty())
            init(a);
    }
    static int mmax(int &x, int &y) {
        return x > y ? x : y;
    }
    static int mmin(int &x, int &y) {
        return x < y ? x : y;
    }
    void init(const vi &a) {
        lg2.resize(a.size(), 0);
        dp.resize(a.size(), vi(log2(a.size() - 1) + 1, 0));
        lg2[0] = -1;
        for (int i = 1; i < a.size(); i++) {
            lg2[i] = lg2[i / 2] + 1;
            dp[i][0] = a[i];
        }
        for (int j = 1; j <= lg2[a.size() - 1]; j++)
            for (int i = 1; i + (1 << j) - 1 < a.size(); i++)
                dp[i][j] = ope[op](dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);
    }
    int query(int l, int r) {
        int k = lg2[r - l + 1];
        return ope[op](dp[l][k], dp[r - (1 << k) + 1][k]);
    }
};

int main() {
    // freopen("in.in", "r", stdin);
    IOinit();
    int n, q;
    cin >> n >> q;
    vi a(n + 1, 0);
    for (int i = 1; i <= n; i++)
        cin >> a[i];
    ST st1(a), st2(a, 1);
    while (q--) {
        int l, r;
        cin >> l >> r;
        cout << st1.query(l, r) - st2.query(l, r) << '\n';
    }
    return 0;
}

P1198 最大数 - 洛谷

P1198 [JSOI2008 最大数 - 洛谷](https://www.luogu.com.cn/problem/P1198)

线段树解法

可将题目看成区间长度为 m m m 的线段树,此时尾插的操作就变成了单点修改 + + + 区间查询的题。

因为这题首先涉及单点修改,且无法使用树状数组维护,所以第 1 个想到的应该是线段树。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using vl = vector<LL>;

const LL INF = -1e18;
struct Segment_tree {
    struct Node {
        int l, r;
        LL mmax;
    };
    vector<Node> sgt;
    Segment_tree(int n) {
        sgt.resize(4 * n + 4, {0, 0, INF});
        build(1, 1, n);
    }
    void build(int p, int l, int r) {
        sgt[p] = {l, r, INF};
        if (l >= r)
            return;
        int mid = (l + r) / 2;
        build(p * 2, l, mid);
        build(p * 2 + 1, mid + 1, r);
    }
    void modify(int p, int x, LL k) {
        if (sgt[p].l == sgt[p].r) {
            sgt[p].mmax = k;
            return;
        }
        int mid = (sgt[p].l + sgt[p].r) / 2;
        if (x <= mid)
            modify(p * 2, x, k);
        else
            modify(p * 2 + 1, x, k);
        adjust_fa(p);
    }
    void adjust_fa(int p) {
        sgt[p].mmax = max(sgt[p * 2].mmax, sgt[p * 2 + 1].mmax);
    }
    LL query(int p, int l, int r) {
        if (l <= sgt[p].l && sgt[p].r <= r)
            return sgt[p].mmax;
        int mid = (sgt[p].l + sgt[p].r) / 2;
        LL ans = INF;
        if (l <= mid)
            ans = max(ans, query(p * 2, l, r));
        if (mid < r)
            ans = max(ans, query(p * 2 + 1, l, r));
        return ans;
    }
};

int main() {
    // freopen("in.in", "r", stdin);
    LL m, MOD;
    cin >> m >> MOD;
    Segment_tree stt(m);
    for (LL i = 1, ip = 0, lans = 0; i <= m; i++) {
        char op;
        LL x;
        cin >> op >> x;
        if (op == 'A') {
            // 尾插
            stt.modify(1, ++ip, (x + lans) % MOD);
        } else {
            // 查询数列的末尾x个元素的最大值
            lans = stt.query(1, ip - x + 1, ip);
            cout << lans << '\n';
        }
    }
    return 0;
}

逆向 ST 表解法

一般情况下是无法使用 ST 表的,因为 ST 表只适合解决静态问题。但这题很是特殊,由于是尾部插入,不会对已维护的 ST 表产生影响,而是每新增一个数,就增加一个 log ⁡ n \log n logn 级别的递推。

例如新插入一个元素 x 6 x_6 x6 :

j = 0 x 1 x 2 x 3 x 4 x 5 j = 1 x 12 x 23 x 34 x 45 j = 2 x 14 x 25 → 插入新元素 x 6 \begin{matrix}j=0&x_1&x_2&x_3&x_4&x_5\\j=1&x_{12}&x_{23}&x_{34}&x_{45}&\\j=2&x_{14}&x_{25}\end{matrix}\xrightarrow{\text{插入新元素}x_6} j=0j=1j=2x1x12x14x2x23x25x3x34x4x45x5插入新元素x6

j = 0 x 1 x 2 x 3 x 4 x 5 x 6 j = 1 x 12 x 23 x 34 x 45 x 56 j = 2 x 14 x 25 x 36 \begin{matrix}j=0&x_1&x_2&x_3&x_4&x_5&x_6\\j=1&x_{12}&x_{23}&x_{34}&x_{45}&x_{56}\\j=2&x_{14}&x_{25}&x_{36}\end{matrix} j=0j=1j=2x1x12x14x2x23x25x3x34x36x4x45x5x56x6

则更新的格子只有 x 6 x_6 x6 相关的格子。但和 x 6 x_6 x6 有关的格子都不好更新,这时可尝试将数据整体右对齐:

j = 0 x 1 x 2 x 3 x 4 x 5 j = 1 x 12 x 23 x 34 x 45 j = 2 x 14 x 25 → 插入新元素 x 6 \begin{matrix}j=0&x_1&x_2&x_3&x_4&x_5\\j=1&&x_{12}&x_{23}&x_{34}&x_{45}&\\j=2&&&&x_{14}&x_{25}\end{matrix}\xrightarrow{\text{插入新元素}x_6} j=0j=1j=2x1x2x12x3x23x4x34x14x5x45x25插入新元素x6

j = 0 x 1 x 2 x 3 x 4 x 5 x 6 j = 1 x 12 x 23 x 34 x 45 x 56 j = 2 x 14 x 25 x 36 \begin{matrix}j=0&x_1&x_2&x_3&x_4&x_5&x_6\\j=1&&x_{12}&x_{23}&x_{34}&x_{45}&x_{56}\\j=2&&&&x_{14}&x_{25}&x_{36}\end{matrix} j=0j=1j=2x1x2x12x3x23x4x34x14x5x45x25x6x56x36

此时只需更新末尾即可。但这样的 ST 表,内部 dp 表的含义需要更改为:

d p i j dpij dpij 表示维护以 i i i 为结尾,长度为 2 j 2^j 2j 的区间的最大值。这种逆向 ST 表。但毕竟这个解法太过冷门,这种题能用线段树就尽量使用线段树。

参考程序如下,也可以当成逆向 ST 表的模板的一种:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using vl = vector<LL>;
using vvl = vector<vl>;

struct ST {
    vl lg2;
    vvl dp; // dp[i][j]表示以i为重点,长度为2^j的区间内的最值
    static const LL INF = -1e18;
    ST(LL n) {
        // 可边插入边更新,但实际操作很困难,不如一开始就开辟足够空间
        lg2.resize(n + 1, 0);
        lg2[0] = -1;
        for (int i = 1; i <= n; i++) // 初始化对数函数值
            lg2[i] = lg2[i / 2] + 1;
        dp.resize(n + 1, vl(log2(n) + 1, INF));
        dp[0][0] = 0; // 作为ST表的长度计量
    }
    void insert(LL x) {
        LL &ip = dp[0][0];
        dp[++ip][0] = x;                  // 尾插进ST表
        for (LL i = 1; i <= lg2[ip]; i++) // 根据当前数据量进行更新
            dp[ip][i] = max(dp[ip][i - 1], dp[ip - (1 << (i - 1))][i - 1]);
    }
    LL query(LL l, LL r) {
        LL k = lg2[r - l + 1];
        return max(dp[r][k], dp[l + (1 << k) - 1][k]);
    }
    LL len() {
        return dp[0][0]; // 获取j=0时的dp表长度
    }
};

int main() {
    // freopen("in.in", "r", stdin);
    LL m, MOD;
    cin >> m >> MOD;
    ST st(m);
    for (LL i = 1, lans = 0; i <= m; i++) {
        char op;
        LL x;
        cin >> op >> x;
        if (op == 'A') {
            st.insert((x + lans) % MOD);
        } else {
            LL len = st.len();
            lans = st.query(len - x + 1, len);
            cout << lans << '\n';
        }
    }
    return 0;
}

P7809 01 序列 - 洛谷

P7809 [JRKSJ R2 01 序列 - 洛谷](https://www.luogu.com.cn/problem/P7809)

看题面的第 1 个棘手的问题就是:最长不下降子序列(LNDS,Longest Non-Decreasing Subsequence)和最长上升子序列(LIS,Longest Increasing Subsequence)无法使用 线段树、树状数组和 ST 表进行维护,但题目却是静态问题 ,此时肯定是需要分析哪些信息需要维护。类似的多种操作的动态、静态问题大都需要如此分析。

  • 首先是最简单的 LIS 的查询(软柿子)。因为题目给的序列只有 0 和 1 ,答案只能是 { 1 , 2 } \{1,2\} {1,2} ,前者表示不存在 01 序列,后者表示存在。此时在输入数据时可初始化数组 orz[i] 表示区间 [1,i] 内的 01 数对的个数(one,zero,orz )。

    然后比较 orz[l-1] 是否等于 orz[r] ,等于的话说明 01 序列全都在 [1,l-1] 这个区间,此时答案是 1;不等的话说明 [l,r] 存在 01 序列的分布,此时答案是 2 。

    这个分析思路可以看成是用动态规划解决这个子问题,即转移方程为

    orz[i]=orz[i-1]+(a[i]==1&&a[i-1]==0)

  • 然后就是硬骨头 LNDS 的查询。因为题目给的序列只有 0 和 1 ,所以 LNDS 有 3 种情况:全为 0 ,全为 1 ,一部分为 0 一部分为 1 。所以这个区间的 LNDS 的长度就是这个区间的三种序列最长的那个

    • 全为 0 和全为 1 比较好处理,直接维护一个 0、1 的出现次数的前缀和,用区间相减的形式查询即可。

    • 一部分为 0 一部分为 1 的 LNDS,这个情况最难分析的,因为 0 的区域和 1 的区域会存在一个不确定的分界线,例如 0000 ∣ 111 0000|111 0000∣111 。这个分界线还会改变,所以想要得到这类子序列中最长的那一个,需要遍历分界线。

      假设这个分界线是 k k k ,则这个 01 序列的全为 0 部分的区间为 l , k l,k l,k ,LNDS 的长度是 z e r o k − z e r o l − 1 zerok-zerol-1 zerok−zerol−1 ,同理全为 1 的部分的区间是 k + 1 , r k+1,r k+1,r ,长度是 o n e r − o n e k oner-onek oner−onek 。将 2 个部分累加在一起就是这个 01 序列内的 LNDS 的长度 o n e r − o n e k + z e r o k − z e r o l − 1 oner-onek+zerok-zerol-1 oner−onek+zerok−zerol−1

      然后遍历所有的 k ∈ [ l , r ) k\in[l,r) k∈[l,r) ,则这个序列的长度的最大值是

      max ( o n e r − o n e k + z e r o k − z e r o l − 1 ) \text{max}(oner-onek+zerok-zerol-1) max(oner−onek+zerok−zerol−1)

      其中 o n e r oner oner 和 o n e l − 1 onel-1 onel−1 已知,可将这 2 项提取出来:

      o n e r − z e r o l − 1 + max ( z e r o k − o n e k ) oner-zerol-1+\text{max}(zerok-onek) oner−zerol−1+max(zerok−onek)

      此时 max ( z e r o k − o n e k ) \text{max}(zerok-onek) max(zerok−onek) 相当于创建了一个新的序列 { z e r o k − o n e k } \{zerok-onek\} {zerok−onek} ,求这个序列的最值,此时就可以使用线段树或 ST 表进行维护。

所以题目需要先维护 3 个数组: o n e , z e r o , o r z one,zero,orz one,zero,orz 和 1 个 ST 表,然后就是一个静态问题。

这个题的数据量极其庞大,使用 scanf 和优化过的 cin 也有超时的可能,需要使用快速读写和快速输出,同时避免使用 vector

P7809 [JRKSJ R2 01 序列 - 洛谷](https://www.luogu.com.cn/problem/P7809) 参考程序:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

template <typename T>
void read(T &x) {
    char ch = getchar();
    x = 0;
    while (ch < '0' || ch > '9')
        ch = getchar();
    while (ch >= '0' && ch <= '9') {
        x = x * 10 + ch - '0';
        ch = getchar();
    }
}

template <typename T>
void print(T x) {
    if (x > 9)
        print(x / 10);
    putchar(x % 10 + '0');
}

const int N = 1e6 + 10;
struct ST {
    int dp[N][25]; // log2(N)<20
	int lg2[N] = {-1};
    void init(int n) {
        for (int i = 1; i <= n; i++)
            lg2[i] = lg2[i / 2] + 1;
        for (int j = 1; j <= lg2[n]; j++)
            for (int i = 1; i + (1 << j) - 1 <= n; i++)
                dp[i][j] = max(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);
    }
    int query(int l, int r) {
        return max(dp[l][lg2[r - l + 1]],
                   dp[r - (1 << lg2[r - l + 1]) + 1][lg2[r - l + 1]]);
    }
};
int orz[N], one[N], zero[N], a[N];
ST st;
int n, m;

int main() {
    // freopen("in.in", "r", stdin);
    read(n), read(m);
    for (int i = 1; i <= n; i++) {
        read(a[i]);
        one[i] = one[i - 1] + (a[i]); // c++将布尔值解释为0或1
        zero[i] = zero[i - 1] + (!a[i]);
        orz[i] = orz[i - 1] + (a[i] == 1 && a[i - 1] == 0);
        st.dp[i][0] = zero[i] - one[i]; // 初始化ST表
    }
    st.init(n); // 初始化ST表
    for (int i = 1; i <= m; i++) {
        int op, l, r;
        read(op), read(l), read(r);
        if (op == 2) {
            print(orz[r] == orz[l] ? 1 : 2);
            putchar('\n');
        } else {
            int ans = max(one[r] - one[l - 1], zero[r] - zero[l - 1]);
            int part3 = one[r] - zero[l - 1] + st.query(l, r);
            print(max(ans, part3));
            putchar('\n');
        }
    }
    return 0;
}

OJ 参考

  1. ST 表建立后只查询的

P3865 【模板】ST 表 & RMQ 问题 - 洛谷

P1890 gcd 区间 - 洛谷 一题多解

P2251 质量检测 - 洛谷 一题多解

P2880 [USACO07JAN Balanced Lineup G - 洛谷](https://www.luogu.com.cn/problem/P2880)

  1. 逆向 ST 表

P1198 [JSOI2008 最大数 - 洛谷](https://www.luogu.com.cn/problem/P1198)

  1. 综合题

P7809 [JRKSJ R2 01 序列 - 洛谷](https://www.luogu.com.cn/problem/P7809)

相关推荐
凡人叶枫28 分钟前
Effective C++ 条款16:成对使用 new 和 delete 时要采取相同形式
开发语言·c++·effective c++
不吃土豆的马铃薯44 分钟前
C++ 高性能网络缓冲区 Buffer 源码解析
linux·服务器·开发语言·网络·c++
.千余1 小时前
【C++】C++继承入门(下):友元、静态成员与菱形继承的底层逻辑
开发语言·c++·笔记·学习·其他
初中就开始混世的大魔王2 小时前
6 Fast DDS-传输层
开发语言·c++·中间件·信息与通信
花间相见3 小时前
【LeetCode02】—— 两数之和:哈希表入门经典详解
数据结构·散列表
代码中介商4 小时前
C++ 智能指针完全指南(三):weak_ptr 与循环引用
开发语言·c++
BestOrNothing_20154 小时前
ROS2 C++ 小车控制完整实战(二):自定义 msg 消息发布与订阅保姆级教程
c++·ros2·subscriber·publisher·msg·topic通信·自定义接口
-森屿安年-4 小时前
91. 解码方法
c++·动态规划
有点。4 小时前
C++(二分答案)
c++
程序喵大人4 小时前
【C++并发系列】第一章:多线程读写同一个变量为什么会出错
开发语言·c++·多线程·并发