[Aya Round 1 H] 破碎的历史

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

一次染色只能填补两个已黑点之间的空隙,因此答案等价于:两端点初始为黑,且每段初始白色空隙都被至少一个活跃操作跨过。

OJ: luogu

题目 ID: P8473

难度:提高+/省选-

标签:线段树二分区间覆盖建模思维

日期: 2026-06-21 02:30

题意

数轴上有 n 个特殊点,每个点初始是黑色或白色。

还维护一个当前活跃的线段集合 S

两类操作:

  • 1 l r:把所有端点都在 [l, r] 内的特殊点线段加入 S
  • 2 x:撤销第 x 次操作加入的所有线段

在初始时和每次操作后,判断是否存在一种合法顺序,使得可以把所有特殊点都染成黑色。

一次合法染色要求:

  • S 中一条线段
  • 其两个端点对应的特殊点都已经是黑色
  • 然后把线段内部所有白色特殊点染黑

思路

先看一个最直接的暴力闭包:

cpp
#include <bits/stdc++.h>
using namespace std;

// brute.cpp:直接维护当前活跃线段集合,然后暴力模拟染色闭包。

const int MAXN = 105;
const int MAXQ = 105;

int n, q;
int pos_arr[MAXN];
int color_arr[MAXN];
int add_l[MAXQ], add_r[MAXQ];
int add_ok[MAXQ];
int cnt_seg[MAXN][MAXN];

bool simulate() {
    int black[MAXN];
    for (int i = 1; i <= n; i++) {
        black[i] = color_arr[i];
    }

    int changed = 1;
    while (changed) {
        changed = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = i; j <= n; j++) {
                if (cnt_seg[i][j] == 0) {
                    continue;
                }
                if (black[i] == 0 || black[j] == 0) {
                    continue;
                }
                for (int k = i; k <= j; k++) {
                    if (black[k] == 0) {
                        black[k] = 1;
                        changed = 1;
                    }
                }
            }
        }
    }

    for (int i = 1; i <= n; i++) {
        if (black[i] == 0) {
            return false;
        }
    }
    return true;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> q;
    for (int i = 1; i <= n; i++) {
        cin >> pos_arr[i];
    }
    for (int i = 1; i <= n; i++) {
        cin >> color_arr[i];
    }

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            cnt_seg[i][j] = 0;
        }
    }

    cout << (simulate() ? "Yes" : "No") << '\n';

    for (int i = 1; i <= q; i++) {
        int op;
        cin >> op;
        add_ok[i] = 0;

        if (op == 1) {
            int l, r;
            cin >> l >> r;

            int L = lower_bound(pos_arr + 1, pos_arr + n + 1, l) - pos_arr;
            int R = (int)(upper_bound(pos_arr + 1, pos_arr + n + 1, r) - pos_arr) - 1;

            if (L <= R) {
                add_ok[i] = 1;
                add_l[i] = L;
                add_r[i] = R;
                for (int x = L; x <= R; x++) {
                    for (int y = x; y <= R; y++) {
                        cnt_seg[x][y]++;
                    }
                }
            }
        } else {
            int x;
            cin >> x;
            if (add_ok[x]) {
                for (int i1 = add_l[x]; i1 <= add_r[x]; i1++) {
                    for (int j1 = i1; j1 <= add_r[x]; j1++) {
                        cnt_seg[i1][j1]--;
                    }
                }
            }
        }

        cout << (simulate() ? "Yes" : "No") << '\n';
    }

    return 0;
}

brute.cpp 显式维护当前有哪些线段活跃,然后不断尝试:

  • 如果某条线段两端已经黑了
  • 就把中间点全部染黑

直到不能继续为止。

这个做法可以帮助理解题意,但数据范围下显然不可能这么做。

关键观察是:

一次染色只能把两个已黑点之间的点染黑。

因此黑色集合只能“填补空隙”,而不会向最左黑点左边或最右黑点右边扩张。

这立刻带来第一个必要条件:

  • 1 个特殊点和第 n 个特殊点必须初始为黑色

接下来只关注初始黑点序列:

b1<b2<<bkb_1 < b_2 < \dots < b_k

如果相邻两个初始黑点 bib_ibi+1b_{i+1} 中间存在白点,那么这段白色空隙想被染黑,就必须有某个活跃操作跨过它。

原因很直接:

  • 在这段空隙第一次被染黑前,里面没有黑点
  • 所以唯一可能的合法线段,端点只能来自这段空隙的左右两侧

反过来,如果某个活跃操作跨过了这段空隙,那么它两侧的初始黑点本来就是黑的,可以直接用这条线段把整段空隙染黑。

于是原题就被压缩成:

  • 两端点初始必须黑
  • 每一段初始白色空隙,都至少被一个活跃操作跨过

现在看一次 1 l r 操作。

在特殊点下标上,它覆盖的是一个连续区间 [L,R][L, R]

如果这段区间里包含黑点序列中的第 ss 到第 tt 个黑点,那么它就能跨过所有空隙:

[s,t1][s, t-1]

这变成了一个非常标准的动态区间覆盖问题:

  • 加入操作:给一段空隙覆盖次数 +1
  • 撤销操作:给同一段空隙覆盖次数 -1
  • 判断当前最小覆盖次数是否大于 0

用线段树维护“所有空隙的最小覆盖次数”即可。

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 500000 + 5;
const int INF = 1000000000;

int n, q;
int pos_arr[MAXN];
int color_arr[MAXN];
int black_idx[MAXN];
int black_cnt;

int seg_min[MAXN << 2];
int lazy_add[MAXN << 2];
int base_gap[MAXN];

int op_l[MAXN];
int op_r[MAXN];
int op_valid[MAXN];

void push_up(int u) {
    seg_min[u] = min(seg_min[u << 1], seg_min[u << 1 | 1]);
}

void build(int u, int l, int r) {
    lazy_add[u] = 0;
    if (l == r) {
        seg_min[u] = base_gap[l];
        return;
    }

    int mid = (l + r) >> 1;
    build(u << 1, l, mid);
    build(u << 1 | 1, mid + 1, r);
    push_up(u);
}

void apply_add(int u, int val) {
    seg_min[u] += val;
    lazy_add[u] += val;
}

void push_down(int u) {
    if (lazy_add[u] == 0) {
        return;
    }
    apply_add(u << 1, lazy_add[u]);
    apply_add(u << 1 | 1, lazy_add[u]);
    lazy_add[u] = 0;
}

void range_add(int u, int l, int r, int ql, int qr, int val) {
    if (ql <= l && r <= qr) {
        apply_add(u, val);
        return;
    }

    push_down(u);
    int mid = (l + r) >> 1;
    if (ql <= mid) {
        range_add(u << 1, l, mid, ql, qr, val);
    }
    if (qr > mid) {
        range_add(u << 1 | 1, mid + 1, r, ql, qr, val);
    }
    push_up(u);
}

bool can_all_black() {
    if (color_arr[1] == 0 || color_arr[n] == 0) {
        return false;
    }
    if (black_cnt <= 1) {
        return n == 1 && color_arr[1] == 1;
    }
    return seg_min[1] > 0;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> q;
    for (int i = 1; i <= n; i++) {
        cin >> pos_arr[i];
    }
    for (int i = 1; i <= n; i++) {
        cin >> color_arr[i];
    }

    black_cnt = 0;
    for (int i = 1; i <= n; i++) {
        if (color_arr[i] == 1) {
            black_idx[++black_cnt] = i;
        }
    }

    int gap_cnt = max(0, black_cnt - 1);
    if (gap_cnt > 0) {
        for (int i = 1; i <= gap_cnt; i++) {
            if (black_idx[i + 1] == black_idx[i] + 1) {
                // 两个黑点相邻,中间没有白点,这个“缝隙”天然已经满足。
                base_gap[i] = INF;
            } else {
                // 中间存在白点,需要至少一个活跃操作跨过这段缝隙。
                base_gap[i] = 0;
            }
        }
        build(1, 1, gap_cnt);
    }

    cout << (can_all_black() ? "Yes" : "No") << '\n';

    for (int i = 1; i <= q; i++) {
        int op;
        cin >> op;
        op_valid[i] = 0;

        if (op == 1) {
            int l, r;
            cin >> l >> r;

            if (gap_cnt > 0) {
                int left_pos = lower_bound(pos_arr + 1, pos_arr + n + 1, l) - pos_arr;
                int right_pos = (int)(upper_bound(pos_arr + 1, pos_arr + n + 1, r) - pos_arr) - 1;

                if (left_pos <= right_pos && black_cnt >= 2) {
                    int left_black = lower_bound(black_idx + 1, black_idx + black_cnt + 1, left_pos) - black_idx;
                    int right_black = (int)(upper_bound(black_idx + 1, black_idx + black_cnt + 1, right_pos) - black_idx) - 1;

                    if (left_black < right_black) {
                        op_valid[i] = 1;
                        op_l[i] = left_black;
                        op_r[i] = right_black - 1;
                        range_add(1, 1, gap_cnt, op_l[i], op_r[i], 1);
                    }
                }
            }
        } else {
            int x;
            cin >> x;
            if (gap_cnt > 0 && op_valid[x]) {
                range_add(1, 1, gap_cnt, op_l[x], op_r[x], -1);
            }
        }

        cout << (can_all_black() ? "Yes" : "No") << '\n';
    }

    return 0;
}

复杂度

每次操作主要做:

  • 二分定位特殊点区间
  • 二分定位黑点序号区间
  • 线段树区间加减

所以单次复杂度:

O(logn)O(log n)

总时间复杂度:

O((n+q)logn)O((n + q) \log n)

空间复杂度:

O(n+q)O(n + q)

总结

这题最关键的不是线段树,而是先把复杂染色过程压缩成一个更简单的判定:

  • 黑色只能填补初始黑点之间的白色空隙

一旦看出这一点,后面就是“动态维护所有空隙是否都被覆盖”的标准模型。

一图流解析

这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

一图流解析