一次染色只能填补两个已黑点之间的空隙,因此答案等价于:两端点初始为黑,且每段初始白色空隙都被至少一个活跃操作跨过。
OJ: luogu
题目 ID: P8473
难度:提高+/省选-
标签:线段树二分区间覆盖建模思维
日期: 2026-06-21 02:30
题意
数轴上有 n 个特殊点,每个点初始是黑色或白色。
还维护一个当前活跃的线段集合 S。
两类操作:
1 l r:把所有端点都在[l, r]内的特殊点线段加入S2 x:撤销第x次操作加入的所有线段
在初始时和每次操作后,判断是否存在一种合法顺序,使得可以把所有特殊点都染成黑色。
一次合法染色要求:
- 选
S中一条线段 - 其两个端点对应的特殊点都已经是黑色
- 然后把线段内部所有白色特殊点染黑
思路
先看一个最直接的暴力闭包:
#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个特殊点必须初始为黑色
接下来只关注初始黑点序列:
如果相邻两个初始黑点
原因很直接:
- 在这段空隙第一次被染黑前,里面没有黑点
- 所以唯一可能的合法线段,端点只能来自这段空隙的左右两侧
反过来,如果某个活跃操作跨过了这段空隙,那么它两侧的初始黑点本来就是黑的,可以直接用这条线段把整段空隙染黑。
于是原题就被压缩成:
- 两端点初始必须黑
- 每一段初始白色空隙,都至少被一个活跃操作跨过
现在看一次 1 l r 操作。
在特殊点下标上,它覆盖的是一个连续区间
如果这段区间里包含黑点序列中的第
这变成了一个非常标准的动态区间覆盖问题:
- 加入操作:给一段空隙覆盖次数
+1 - 撤销操作:给同一段空隙覆盖次数
-1 - 判断当前最小覆盖次数是否大于
0
用线段树维护“所有空隙的最小覆盖次数”即可。
代码
#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;
}复杂度
每次操作主要做:
- 二分定位特殊点区间
- 二分定位黑点序号区间
- 线段树区间加减
所以单次复杂度:
总时间复杂度:
空间复杂度:
总结
这题最关键的不是线段树,而是先把复杂染色过程压缩成一个更简单的判定:
- 黑色只能填补初始黑点之间的白色空隙
一旦看出这一点,后面就是“动态维护所有空隙是否都被覆盖”的标准模型。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。






