[JRKSJ R2] 01 序列

GitHub跳转原题关系图返回列表

把 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] 的最长上升子序列长度

数据范围非常大,n10^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],说明确实存在 01 前面,答案就是 2;否则答案是 1

这两个查询都能做到 O(1)O(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_zeroprev_oneO(n)O(n)
  • 建 ST 表:O(nlogn)O(n log n)
  • 每次询问:O(1)O(1)

总时间复杂度 O(nlogn+m)O(n log n + m),空间复杂度 O(nlogn)O(n log n)

总结

这题的关键不是套普通 LIS 模板,而是抓住 01 串的结构非常简单:

  • 不下降子序列一定是“前面一段 0,后面一段 1”
  • 严格上升子序列最多只有 0,1 两个数

把这两个性质用前缀和和位置预处理表达出来以后,问题就能压成常数时间查询。