在线段树中同时维护区间 1 的数量和最长连续 0,并用左优先递归把目标区间最靠前的若干个 0 填成 1。
OJ: luogu
题目 ID: P4344
难度:提高+/省选-
标签:线段树懒标记区间赋值区间最值模拟
日期: 2026-06-21 03:17
题意
初始有一个长度为 n 的 01 序列,开始时全是 1。
支持三类操作:
- 把一段区间全部变成
0 - 把一段区间里的正常脑组织挖出来,拿去填另一段区间里最靠左的脑洞
- 查询一段区间中最长连续
0的长度
思路
先看一个可以直接验证想法的朴素解:
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 205;
int n, m;
int a[MAXN];
int query_best_zero(int l, int r) {
int best = 0;
int cur = 0;
for (int i = l; i <= r; i++) {
if (a[i] == 0) {
cur++;
best = max(best, cur);
} else {
cur = 0;
}
}
return best;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// 这是一个直接模拟:
// 把区间真的改掉,再线性统计最长连续 0。
cin >> n >> m;
for (int i = 1; i <= n; i++) {
a[i] = 1;
}
while (m--) {
int op;
cin >> op;
if (op == 0) {
int l, r;
cin >> l >> r;
for (int i = l; i <= r; i++) {
a[i] = 0;
}
} else if (op == 1) {
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
int healthy_cnt = 0;
for (int i = x1; i <= y1; i++) {
if (a[i] == 1) {
healthy_cnt++;
}
}
for (int i = x1; i <= y1; i++) {
a[i] = 0;
}
for (int i = x2; i <= y2 && healthy_cnt > 0; i++) {
if (a[i] == 0) {
a[i] = 1;
healthy_cnt--;
}
}
} else {
int l, r;
cin >> l >> r;
cout << query_best_zero(l, r) << '\n';
}
}
return 0;
}brute.cpp 完全按题意模拟:
- 统计供体区间里有多少个
1 - 清空供体区间
- 从左到右扫描目标区间,把最靠前的
0填成1 - 查询时暴力扫最长连续
0
这个思路容易理解,但大数据下显然太慢。
这题的关键是把所有操作都放进线段树。
线段树每个节点需要维护:
- 区间里
1的数量 - 前缀连续
0 - 后缀连续
0 - 最长连续
0
这样:
- 区间清空就是整段赋值为
0 - 查询最长脑洞就是查区间
best_zero
操作 1 虽然特殊,但也可以拆开:
- 先查供体区间里当前有多少个
1 - 把供体区间整段设为
0 - 再把目标区间里最靠左的若干个
0改成1
第三步用一个左优先递归就能实现:
- 如果某个完整节点里的
0数量已经不超过还需填的数量,就整段直接设成1 - 否则继续优先递归左儿子,再递归右儿子
这正好对应题意里“尽量填补位置比较靠前的脑洞”。
可以用下面这个小表格理解样例里的第一次治疗:
| 步骤 | 区间状态 |
|---|---|
| 初始挖洞后 | 1 0 1 0 0 0 1 1 1 0 |
挖出 [8,10] 后拿到的正常组织数 |
2 |
清空 [8,10] 后 |
1 0 1 0 0 0 1 0 0 0 |
从左到右填补 [1,4] 后 |
1 1 1 1 0 0 1 0 0 0 |
这张表里最重要的是最后一步:
目标区间不会把已有的 1 改掉,而是只会把最靠左的两个 0 补上。
这正是代码里 fill_leftmost_zero() 的行为。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200000 + 5;
struct NodeInfo {
int len;
int one_cnt;
int pre_zero;
int suf_zero;
int best_zero;
};
int n, m;
int one_cnt[MAXN << 2];
int pre_zero[MAXN << 2];
int suf_zero[MAXN << 2];
int best_zero[MAXN << 2];
int lazy_tag[MAXN << 2];
NodeInfo merge_info(const NodeInfo &left_info, const NodeInfo &right_info) {
if (left_info.len == 0) {
return right_info;
}
if (right_info.len == 0) {
return left_info;
}
NodeInfo res;
res.len = left_info.len + right_info.len;
res.one_cnt = left_info.one_cnt + right_info.one_cnt;
res.pre_zero = left_info.pre_zero;
if (left_info.pre_zero == left_info.len) {
res.pre_zero = left_info.len + right_info.pre_zero;
}
res.suf_zero = right_info.suf_zero;
if (right_info.suf_zero == right_info.len) {
res.suf_zero = right_info.len + left_info.suf_zero;
}
res.best_zero = max(left_info.best_zero, right_info.best_zero);
res.best_zero = max(res.best_zero, left_info.suf_zero + right_info.pre_zero);
return res;
}
void apply_set(int u, int l, int r, int val) {
lazy_tag[u] = val;
if (val == 0) {
one_cnt[u] = 0;
pre_zero[u] = r - l + 1;
suf_zero[u] = r - l + 1;
best_zero[u] = r - l + 1;
} else {
one_cnt[u] = r - l + 1;
pre_zero[u] = 0;
suf_zero[u] = 0;
best_zero[u] = 0;
}
}
void push_up(int u, int l, int r) {
int mid = (l + r) >> 1;
NodeInfo left_info;
left_info.len = mid - l + 1;
left_info.one_cnt = one_cnt[u << 1];
left_info.pre_zero = pre_zero[u << 1];
left_info.suf_zero = suf_zero[u << 1];
left_info.best_zero = best_zero[u << 1];
NodeInfo right_info;
right_info.len = r - mid;
right_info.one_cnt = one_cnt[u << 1 | 1];
right_info.pre_zero = pre_zero[u << 1 | 1];
right_info.suf_zero = suf_zero[u << 1 | 1];
right_info.best_zero = best_zero[u << 1 | 1];
NodeInfo res = merge_info(left_info, right_info);
one_cnt[u] = res.one_cnt;
pre_zero[u] = res.pre_zero;
suf_zero[u] = res.suf_zero;
best_zero[u] = res.best_zero;
}
void push_down(int u, int l, int r) {
if (lazy_tag[u] == -1 || l == r) {
return;
}
int mid = (l + r) >> 1;
apply_set(u << 1, l, mid, lazy_tag[u]);
apply_set(u << 1 | 1, mid + 1, r, lazy_tag[u]);
lazy_tag[u] = -1;
}
void build(int u, int l, int r) {
lazy_tag[u] = -1;
if (l == r) {
// 初始时所有位置都正常工作,也就是全是 1。
apply_set(u, l, r, 1);
return;
}
int mid = (l + r) >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
push_up(u, l, r);
}
void range_set(int u, int l, int r, int ql, int qr, int val) {
if (ql <= l && r <= qr) {
apply_set(u, l, r, val);
return;
}
push_down(u, l, r);
int mid = (l + r) >> 1;
if (ql <= mid) {
range_set(u << 1, l, mid, ql, qr, val);
}
if (qr > mid) {
range_set(u << 1 | 1, mid + 1, r, ql, qr, val);
}
push_up(u, l, r);
}
NodeInfo query_info(int u, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) {
NodeInfo res;
res.len = r - l + 1;
res.one_cnt = one_cnt[u];
res.pre_zero = pre_zero[u];
res.suf_zero = suf_zero[u];
res.best_zero = best_zero[u];
return res;
}
push_down(u, l, r);
int mid = (l + r) >> 1;
if (qr <= mid) {
return query_info(u << 1, l, mid, ql, qr);
}
if (ql > mid) {
return query_info(u << 1 | 1, mid + 1, r, ql, qr);
}
NodeInfo left_info = query_info(u << 1, l, mid, ql, qr);
NodeInfo right_info = query_info(u << 1 | 1, mid + 1, r, ql, qr);
return merge_info(left_info, right_info);
}
void fill_leftmost_zero(int u, int l, int r, int ql, int qr, int &need) {
if (need == 0 || qr < l || r < ql) {
return;
}
int zero_cnt = (r - l + 1) - one_cnt[u];
if (zero_cnt == 0) {
return;
}
if (ql <= l && r <= qr && zero_cnt <= need) {
apply_set(u, l, r, 1);
need -= zero_cnt;
return;
}
if (l == r) {
apply_set(u, l, r, 1);
need--;
return;
}
push_down(u, l, r);
int mid = (l + r) >> 1;
if (ql <= mid) {
fill_leftmost_zero(u << 1, l, mid, ql, qr, need);
}
if (qr > mid && need > 0) {
fill_leftmost_zero(u << 1 | 1, mid + 1, r, ql, qr, need);
}
push_up(u, l, r);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
build(1, 1, n);
while (m--) {
int op;
cin >> op;
if (op == 0) {
int l, r;
cin >> l >> r;
range_set(1, 1, n, l, r, 0);
} else if (op == 1) {
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
NodeInfo source_info = query_info(1, 1, n, x1, y1);
int healthy_cnt = source_info.one_cnt;
// 先把供体区间全部挖空,再把这些 1 从左到右填到目标区间的脑洞里。
range_set(1, 1, n, x1, y1, 0);
fill_leftmost_zero(1, 1, n, x2, y2, healthy_cnt);
} else {
int l, r;
cin >> l >> r;
NodeInfo ans = query_info(1, 1, n, l, r);
cout << ans.best_zero << '\n';
}
}
return 0;
}复杂度
建树是
三类操作都可以控制在
总结
这题表面上是一个很怪的治疗过程,实际上核心就是两件事:
- 线段树维护最长连续
0 - 用左优先递归实现“填最靠前的脑洞”
只要把操作 1 拆成“统计 + 清空 + 左优先填补”,整题就顺了。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。
