在线 ST 表按“以位置结尾”分层预存 2^k 段最大值:追加时增量补层 O(log m),询问用两段幂等覆盖 O(1) 取出后缀最大值。
OJ: roj
题目 ID: 1549
难度:普及+/提高-
标签:ST表RMQ倍增python
创建: 2026-09-30 17:30
更新: 2026-10-07 12:15
形式化题目
维护一个初始为空的序列
- 追加
:在末尾写入 ,其中 是上一次询问的答案(还没询问过时 ); - 询问
:输出当前序列最后 个数的最大值,即下标区间 (0-based, 为当前长度)上的最大值。
序列只在末尾增长,没有修改与删除;而每个新元素的值又依赖上一次询问的答案,所以操作必须严格按输入顺序在线处理,无法离线重排。题目保证第一个操作是追加,且每次询问的
样例的取值依赖可以逐步展开:A 97 追加 Q 1 答 A 17 追加 A 63 追加 A 99 在答案
正解
思路
一句话本质:把"最后
朴素做法:每次 Q L 从下标
瓶颈:不同询问反复比较同一批后缀元素。但序列只追加不修改——历史区间上的比较结果永不失效,完全可以预存下来复用;又因为
分层预存:只存长度为
本题查询的永远是"以末尾结尾的后缀",所以按区间结尾分层比按起点分层更顺手:新元素落在下标
| 层 |
块长 |
结尾 0 | 结尾 1 | 结尾 2 | 结尾 3 |
|---|---|---|---|---|---|
| — | |||||
| — | — | — |
读表要点:每格 = 上一层同结尾的格(后半段)与上一层左移 max——结尾 1 的
查询两段覆盖:询问最后 bit_length 求,不碰浮点),取两段长度都是
| 取块 | 覆盖区间 | 依据 |
|---|---|---|
| 以末尾结尾的 |
||
| 从询问左端起头的 |
由 Q 3 为例(序列 max 得
在线依赖:取值依赖只影响"追加什么值"(last_ans 参与取模),不影响"怎么查";操作按输入顺序原样处理即可。边界上,
代码
st[k][j] 存下标 while 循环是增量建层递推式,询问分支三行就是上面的两段覆盖;last_ans 保存上一次询问答案,供下一个 A 取模使用。
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 17:36
# update_at: 2026-09-30 17:36
import sys
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
op_count, mod = int(next(data)), int(next(data))
# 在线 ST 表:st[k][j] 表示以下标 j + 2^k - 1 结尾、长度 2^k 的区间的最大值。
# 第 k 层只在序列够长(下标 >= 2^k - 1)后才存在,故存下标 = 位置 - 2^k + 1。
st: list[list[int]] = [[]]
out: list[str] = []
size = 0 # 当前序列长度
last_ans = 0 # 上一次询问的答案 a,还没询问过时为 0
for _ in range(op_count):
op = next(data)
arg = int(next(data))
if op == b'A':
st[0].append((arg + last_ans) % mod) # 长度 1 的区间就是新元素本身
pos = size # 新元素的下标
size += 1
k = 1
while (1 << k) <= size: # 区间必须整体落在序列内
if k == len(st): # 序列长度越过 2^k,新建一层
st.append([])
half = 1 << (k - 1)
# 前半段 [pos-2*half+1, pos-half] 与后半段 [pos-half+1, pos] 各取最大再合并
st[k].append(max(st[k - 1][pos - 2 * half + 1], st[k - 1][pos - half + 1]))
k += 1
else:
# 询问最后 L 个数:用首尾两段长 2^k <= L 的区间夹住 [size-L, size-1]
k = arg.bit_length() - 1
span = 1 << k
tail = st[k][size - span] # 以末尾结尾的 2^k 段
head = st[k][size - arg] # 从询问左端起的 2^k 段
last_ans = max(tail, head)
out.append(str(last_ans))
print('\n'.join(out))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 07:39
* update_at: 2026-10-05 07:39
*/
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int MAXM = 200005; // m 的上限
const int MAXK = 20; // 2^18 > 2*10^5,够放下所有层
// 在线 ST 表:st[k][j] 表示以下标 j + 2^k - 1 结尾、长度 2^k 的区间的最大值。
// 第 k 层只在序列够长后才存在,所以存下标 = 结尾位置 - 2^k + 1;
// len[k] 记录第 k 层当前已存了几个表项,新增表项就放在下标 len[k] 处。
ll st[MAXK][MAXM];
int len[MAXK];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
ll m, p;
cin >> m >> p;
ll size = 0; // 当前序列长度
ll last_ans = 0; // 上一次询问的答案 a,还没询问过时为 0
for (ll i = 1; i <= m; i++) {
char op;
ll arg;
cin >> op >> arg;
if (op == 'A') {
// 长度 1 的区间就是新元素本身
st[0][len[0]] = (arg + last_ans) % p;
len[0]++;
ll pos = size; // 新元素的下标
size++;
// 增量补层:新元素只影响“以 pos 结尾”的块
for (int k = 1; (1LL << k) <= size; k++) {
ll half = 1LL << (k - 1); // 半段长度
// 两段合成:以 pos-half 结尾的前半段 + 以 pos 结尾的后半段
st[k][len[k]] = max(st[k - 1][pos - 2 * half + 1],
st[k - 1][pos - half + 1]);
len[k]++;
}
} else {
// 询问最后 arg 个数:用两段长 2^k <= arg 的块夹住 [size-arg, size-1]
int k = 0;
while ((1LL << (k + 1)) <= arg) {
k++;
}
ll span = 1LL << k;
ll tail = st[k][size - span]; // 以末尾结尾的 2^k 段
ll head = st[k][size - arg]; // 从询问左端起头的 2^k 段
last_ans = max(tail, head);
cout << last_ans << '\n';
}
}
return 0;
}复杂度
- 时间复杂度:追加补
层、每层一次 max,全程;询问两次查表 + 一次 max,。与 C++ 线段树/ST 表解法同阶,查询更优。 - 空间复杂度:
个表项(第 层 个, 时合计约 );大量表项是对同一个 int 对象的引用,实测峰值 62.4 MB,低于 128 MB 限制。
总结
后缀 RMQ 在线化:序列只追加不修改 → 历史块可预存;bit_length 取 max 得 last_ans 变量,按输入顺序处理自然满足。实测 8 组真实数据全部一致,最慢一组 0.263 s、峰值内存 62.4 MB,相对 1000 ms / 128 MB 的限制余量充足(本地按 python-oj-short 规则放宽到 20 s 限时、不限内存验证)。