数据结构——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 ] dp[i][j] dp[i][j] 表示区间 [ 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 ] dp[i][j] dp[i][j] 代表的含义为:从 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 ] dp[l][k] dp[l][k] 和 d p [ r − ( 1 < < k ) + 1 ] [ k ] dp[r - (1 << k) + 1][k] dp[r−(1<<k)+1][k] 两个格子,取最大值即可,即 max [ l , r ] = max ( d p [ l , k ] , d p [ r − ( 1 < < k ) + 1 ] [ k ] ) \text{max}_{[l,r]}=\text{max}(dp[l,k],dp[r - (1 << k) + 1][k]) max[l,r]=max(dp[l,k],dp[r−(1<<k)+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 ] dp[i][j] dp[i][j] 表示:从 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 ] ) dp[i][j] = \max(dp[i][j-1], dp[i+(1<<(j-1))][j-1]) dp[i][j]=max(dp[i][j−1],dp[i+(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 ] ) dp[i][j]=\text{gcd}(dp[i][k],dp[k+1][j]) dp[i][j]=gcd(dp[i][k],dp[k+1][j]) ,分界点 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 using namespace std; using vi = vector; using vvi = vector>; void IOinit() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); } struct ST { vi lg2; vvi dp; vector 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 using namespace std; using LL = long long; using vl = vector; const LL INF = -1e18; struct Segment_tree { struct Node { int l, r; LL mmax; }; vector 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 \] dp\[i\]\[j\] dp\[i\]\[j\] 表示维护以 i i i 为结尾,长度为 2 j 2\^j 2j 的区间的最大值。这种逆向 ST 表。但毕竟这个解法太过冷门,这种题能用线段树就尽量使用线段树。 参考程序如下,也可以当成逆向 ST 表的模板的一种: ```cpp #include using namespace std; using LL = long long; using vl = vector; using vvl = vector; 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 \] zero\[k\]-zero\[l-1\] zero\[k\]−zero\[l−1\] ,同理全为 1 的部分的区间是 \[ k + 1 , r \] \[k+1,r\] \[k+1,r\] ,长度是 o n e \[ r \] − o n e \[ k \] one\[r\]-one\[k\] one\[r\]−one\[k\] 。将 2 个部分累加在一起就是这个 01 序列内的 LNDS 的长度 o n e \[ r \] − o n e \[ k \] + z e r o \[ k \] − z e r o \[ l − 1 \] one\[r\]-one\[k\]+zero\[k\]-zero\[l-1\] one\[r\]−one\[k\]+zero\[k\]−zero\[l−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}(one\[r\]-one\[k\]+zero\[k\]-zero\[l-1\]) max(one\[r\]−one\[k\]+zero\[k\]−zero\[l−1\]) 其中 o n e \[ r \] one\[r\] one\[r\] 和 o n e \[ l − 1 \] one\[l-1\] one\[l−1\] 已知,可将这 2 项提取出来: o n e \[ r \] − z e r o \[ l − 1 \] + max ( z e r o \[ k \] − o n e \[ k \] ) one\[r\]-zero\[l-1\]+\\text{max}(zero\[k\]-one\[k\]) one\[r\]−zero\[l−1\]+max(zero\[k\]−one\[k\]) 此时 max ( z e r o \[ k \] − o n e \[ k \] ) \\text{max}(zero\[k\]-one\[k\]) max(zero\[k\]−one\[k\]) 相当于创建了一个新的序列 { z e r o \[ k \] − o n e \[ k \] } \\{zero\[k\]-one\[k\]\\} {zero\[k\]−one\[k\]} ,求这个序列的最值,此时就可以使用线段树或 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 using namespace std; template 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 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 问题 - 洛谷](https://www.luogu.com.cn/problem/P3865) [P1890 gcd 区间 - 洛谷](https://www.luogu.com.cn/problem/P1890) 一题多解 [P2251 质量检测 - 洛谷](https://www.luogu.com.cn/problem/P2251) 一题多解 \[P2880 [USACO07JAN\] Balanced Lineup G - 洛谷](https://www.luogu.com.cn/problem/P2880) 2. 逆向 ST 表 \[P1198 [JSOI2008\] 最大数 - 洛谷](https://www.luogu.com.cn/problem/P1198) 3. 综合题 \[P7809 [JRKSJ R2\] 01 序列 - 洛谷](https://www.luogu.com.cn/problem/P7809)

相关推荐
是梦终空1162 小时前
模板编译期机器学习
开发语言·c++·算法
艾莉丝努力练剑2 小时前
文件描述符fd:跨进程共享机制
java·linux·运维·服务器·开发语言·c++
tankeven2 小时前
NxN棋盘问题00:对角线特性
c++·算法
2501_911088232 小时前
C++中的代理模式变体
开发语言·c++·算法
无限进步_2 小时前
【C++】只出现一次的数字 III:位运算的巧妙应用
数据结构·c++·git·算法·leetcode·github·visual studio
2401_900151542 小时前
代码覆盖率工具实战
开发语言·c++·算法
bu_shuo2 小时前
在命令行中编译cpp文件
开发语言·c++·cpp
CAACoder2 小时前
CATIA/3DE CAA二次开发-ScrollWindow滚动窗口
开发语言·c++·mfc·滚动窗口
ht巷子2 小时前
asio::ip::tcp学习
网络·c++·tcp/ip