把 01 串的最长不下降子序列转成前缀差值区间最大值,最长上升子序列则只需判断是否存在 0 在 1 前面。
OJ: luogu
题目 ID: P7809
难度:提高+/省选-
标签:前缀和ST表思维区间最值
日期: 2026-06-21 14:58
题意
给定一个只含 0/1 的序列,回答两类区间询问:
1 l r:求a[l..r]的最长不下降子序列长度2 l r:求a[l..r]的最长上升子序列长度
数据范围非常大,n 到 10^6,询问次数到 5 * 10^6,所以每次询问必须非常快。
思路
先看一个可以直接验证想法的朴素解:
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 205;
int n, m;
int a[MAXN];
int solve_non_decreasing(int l, int r) {
int answer = 0;
// 枚举分界点 k:取左边全部 0,右边全部 1。
for (int k = l - 1; k <= r; k++) {
int cnt0 = 0;
int cnt1 = 0;
for (int i = l; i <= k; i++) {
if (a[i] == 0) {
cnt0++;
}
}
for (int i = k + 1; i <= r; i++) {
if (a[i] == 1) {
cnt1++;
}
}
answer = max(answer, cnt0 + cnt1);
}
return answer;
}
int solve_increasing(int l, int r) {
int answer = 1;
for (int i = l; i <= r; i++) {
for (int j = i + 1; j <= r; j++) {
if (a[i] < a[j]) {
answer = 2;
}
}
}
return answer;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
while (m--) {
int op, l, r;
cin >> op >> l >> r;
if (op == 1) {
cout << solve_non_decreasing(l, r) << '\n';
}
else {
cout << solve_increasing(l, r) << '\n';
}
}
return 0;
}如果直接在每个区间里做 LIS,哪怕只利用 01 串的特殊性,也还是太慢。
1. 最长不下降子序列
在 01 串里,不下降子序列只能长成:
text
若干个 0 + 若干个 1所以只要枚举一个分界点 k:
- 从
l..k里取所有0 - 从
k+1..r里取所有1
答案就是:
text
max( [l..k] 中 0 的个数 + [k+1..r] 中 1 的个数 )设 pre[i] 表示前 i 个数里 1 的个数,那么:
[l..k]中0的个数是(k-l+1) - (pre[k]-pre[l-1])[k+1..r]中1的个数是pre[r] - pre[k]
两项相加后得到:
text
pre[l-1] + pre[r] - l + 1 + (k - 2 * pre[k])对于一次询问,前面那一大段是常数,所以问题就变成:
text
在区间 [l-1, r] 中求 max( k - 2 * pre[k] )这是静态区间最大值,直接用 ST 表维护即可。
2. 最长上升子序列
严格上升的 01 子序列只能是:
text
0, 1所以答案只可能是:
2:区间里存在一个0出现在某个1的前面1:否则只能取单个元素
于是只要预处理:
next_zero[i]:从i开始往后第一个0的位置prev_one[i]:到i为止最后一个1的位置
对于区间 [l, r],如果 next_zero[l] < prev_one[r],说明确实存在 0 在 1 前面,答案就是 2;否则答案是 1。
这两个查询都能做到
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1000005;
const int MAXLOG = 21;
const int BUF_SIZE = 1 << 20;
int n, m;
unsigned char a[MAXN];
int pre_one[MAXN];
int lg2_table[MAXN];
int f[MAXN]; // f[i] = i - 2 * 前 i 个 1 的个数
int st[MAXLOG][MAXN]; // 区间最大值 ST 表,维护 f
int next_zero[MAXN];
int prev_one_pos[MAXN];
char outbuf[BUF_SIZE];
int outpos = 0;
namespace FastIO {
char inbuf[BUF_SIZE];
int inpos = 0;
int inlen = 0;
inline int gc() {
if (inpos == inlen) {
inlen = fread(inbuf, 1, BUF_SIZE, stdin);
inpos = 0;
if (inlen == 0) {
return EOF;
}
}
return inbuf[inpos++];
}
template <typename T>
inline void read(T &x) {
x = 0;
int sign = 1;
int c = gc();
while (c != '-' && (c < '0' || c > '9')) {
c = gc();
}
if (c == '-') {
sign = -1;
c = gc();
}
while (c >= '0' && c <= '9') {
x = x * 10 + c - '0';
c = gc();
}
x *= sign;
}
}
inline void flush_output() {
fwrite(outbuf, 1, outpos, stdout);
outpos = 0;
}
inline void put_char(int c) {
if (outpos == BUF_SIZE) {
flush_output();
}
outbuf[outpos++] = (char)c;
}
inline void write_int(int x) {
if (x == 0) {
put_char('0');
put_char('\n');
return;
}
if (x < 0) {
put_char('-');
x = -x;
}
char s[20];
int len = 0;
while (x > 0) {
s[len++] = (char)('0' + x % 10);
x /= 10;
}
for (int i = len - 1; i >= 0; i--) {
put_char(s[i]);
}
put_char('\n');
}
void build_logs() {
lg2_table[1] = 0;
for (int i = 2; i <= n + 1; i++) {
lg2_table[i] = lg2_table[i >> 1] + 1;
}
}
void build_sparse_table() {
for (int i = 0; i <= n; i++) {
st[0][i] = f[i];
}
for (int k = 1; k < MAXLOG; k++) {
int len = 1 << k;
int half = len >> 1;
if (len > n + 1) {
break;
}
for (int i = 0; i + len - 1 <= n; i++) {
st[k][i] = max(st[k - 1][i], st[k - 1][i + half]);
}
}
}
inline int query_max(int l, int r) {
int k = lg2_table[r - l + 1];
return max(st[k][l], st[k][r - (1 << k) + 1]);
}
inline int solve_non_decreasing(int l, int r) {
// 枚举“0 段”和“1 段”的分界点 k:
// 取 [l, k] 中所有 0,再取 [k+1, r] 中所有 1。
int best = query_max(l - 1, r);
return pre_one[l - 1] + pre_one[r] - l + 1 + best;
}
inline int solve_increasing(int l, int r) {
// 01 串的严格上升子序列只可能是 0,1,长度最多为 2。
// 只要区间里存在一个 0 在某个 1 前面,答案就是 2,否则是 1。
if (next_zero[l] < prev_one_pos[r]) {
return 2;
}
return 1;
}
int main() {
FastIO::read(n);
FastIO::read(m);
pre_one[0] = 0;
prev_one_pos[0] = 0;
for (int i = 1; i <= n; i++) {
int x;
FastIO::read(x);
a[i] = (unsigned char)x;
pre_one[i] = pre_one[i - 1] + x;
prev_one_pos[i] = (x == 1 ? i : prev_one_pos[i - 1]);
}
next_zero[n + 1] = n + 1;
for (int i = n; i >= 1; i--) {
if (a[i] == 0) {
next_zero[i] = i;
}
else {
next_zero[i] = next_zero[i + 1];
}
}
for (int i = 0; i <= n; i++) {
f[i] = i - (pre_one[i] << 1);
}
build_logs();
build_sparse_table();
for (int i = 1; i <= m; i++) {
int op, l, r;
FastIO::read(op);
FastIO::read(l);
FastIO::read(r);
if (op == 1) {
write_int(solve_non_decreasing(l, r));
}
else {
write_int(solve_increasing(l, r));
}
}
flush_output();
return 0;
}复杂度
- 预处理前缀和、
next_zero、prev_one: - 建 ST 表:
- 每次询问:
总时间复杂度
总结
这题的关键不是套普通 LIS 模板,而是抓住 01 串的结构非常简单:
- 不下降子序列一定是“前面一段 0,后面一段 1”
- 严格上升子序列最多只有
0,1两个数
把这两个性质用前缀和和位置预处理表达出来以后,问题就能压成常数时间查询。