用线段树维护区间里空地数与树苗数,把状态分成老树、空地、树苗三类,就能同时处理整段砍树、局部补种和树苗损失统计。
OJ: luogu
题目 ID: P1276
难度:普及/提高-
标签:线段树区间模拟推导
日期: 2026-06-21 01:35
题意
从位置 0 到 L,每个位置一开始都有一棵老树。
有两种操作:
0 A B:把区间[A, B]上所有树都砍掉1 C D:把区间[C, D]上所有空地补种成树苗
最后要输出:
- 最终剩下多少棵树苗
- 曾经种上、后来又被砍掉的树苗有多少棵
思路
先看一个可以直接验证想法的朴素解:
cpp
#include <bits/stdc++.h>
using namespace std;
// brute.cpp:直接按每个位置模拟。
// 0 表示老树,1 表示空地,2 表示树苗。
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int L, n;
cin >> L >> n;
vector<int> state(L + 1, 0);
int removed_saplings = 0;
for (int i = 1; i <= n; i++) {
int op, l, r;
cin >> op >> l >> r;
if (op == 0) {
for (int j = l; j <= r; j++) {
if (state[j] == 2) {
removed_saplings++;
}
state[j] = 1;
}
} else {
for (int j = l; j <= r; j++) {
if (state[j] == 1) {
state[j] = 2;
}
}
}
}
int final_saplings = 0;
for (int i = 0; i <= L; i++) {
if (state[i] == 2) {
final_saplings++;
}
}
cout << final_saplings << '\n' << removed_saplings << '\n';
return 0;
}brute.cpp 直接按位置维护三种状态:
- 老树
- 空地
- 树苗
每次操作逐点更新即可。
这题数据范围其实不大,直接模拟也能通过。
不过如果把它当作线段树练习题,会更有价值。
关键在于把每个位置的状态分成三类:
- 老树
- 空地
- 树苗
在线段树的每个区间上维护:
- 这段里有多少空地
- 这段里有多少树苗
然后分类处理:
- 砍树操作
- 整段都会变成空地
- 如果这段里原本有树苗,它们都会被砍掉,所以要先累计到答案里
- 补种操作
- 只有空地才能变成树苗
- 如果某段已经没有空地,可以直接跳过
- 如果某段全是空地,可以整段直接改成树苗
实现上再配一个区间统一状态标记,就能完成这些覆盖操作。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXL = 10005;
int L, n;
int empty_cnt[MAXL << 2]; // 当前区间里空地数量
int sapling_cnt[MAXL << 2]; // 当前区间里树苗数量
int tag[MAXL << 2]; // -1 混合,0 全是老树,1 全是空地,2 全是树苗
int removed_saplings; // 被种上后又被砍掉的树苗总数
void apply_tag(int u, int l, int r, int value) {
tag[u] = value;
int len = r - l + 1;
if (value == 0) {
empty_cnt[u] = 0;
sapling_cnt[u] = 0;
} else if (value == 1) {
empty_cnt[u] = len;
sapling_cnt[u] = 0;
} else {
empty_cnt[u] = 0;
sapling_cnt[u] = len;
}
}
void push_up(int u) {
empty_cnt[u] = empty_cnt[u << 1] + empty_cnt[u << 1 | 1];
sapling_cnt[u] = sapling_cnt[u << 1] + sapling_cnt[u << 1 | 1];
if (tag[u << 1] == tag[u << 1 | 1] && tag[u << 1] != -1) {
tag[u] = tag[u << 1];
} else {
tag[u] = -1;
}
}
void push_down(int u, int l, int r) {
if (tag[u] == -1 || l == r) {
return;
}
int mid = (l + r) >> 1;
apply_tag(u << 1, l, mid, tag[u]);
apply_tag(u << 1 | 1, mid + 1, r, tag[u]);
tag[u] = -1;
}
void build(int u, int l, int r) {
// 初始时每个位置都是老树。
apply_tag(u, l, r, 0);
if (l == r) {
return;
}
int mid = (l + r) >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
}
// 区间砍树:无论老树还是树苗都清空。
void cut_range(int u, int l, int r, int ql, int qr) {
if (ql <= l && r <= qr) {
removed_saplings += sapling_cnt[u];
apply_tag(u, l, r, 1);
return;
}
push_down(u, l, r);
int mid = (l + r) >> 1;
if (ql <= mid) {
cut_range(u << 1, l, mid, ql, qr);
}
if (qr > mid) {
cut_range(u << 1 | 1, mid + 1, r, ql, qr);
}
push_up(u);
}
// 区间补种:只在空地位置种上树苗。
void plant_range(int u, int l, int r, int ql, int qr) {
if (empty_cnt[u] == 0) {
return;
}
if (ql <= l && r <= qr && tag[u] == 1) {
apply_tag(u, l, r, 2);
return;
}
if (l == r) {
apply_tag(u, l, r, 2);
return;
}
push_down(u, l, r);
int mid = (l + r) >> 1;
if (ql <= mid) {
plant_range(u << 1, l, mid, ql, qr);
}
if (qr > mid) {
plant_range(u << 1 | 1, mid + 1, r, ql, qr);
}
push_up(u);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> L >> n;
build(1, 0, L);
for (int i = 1; i <= n; i++) {
int op, l, r;
cin >> op >> l >> r;
if (op == 0) {
cut_range(1, 0, L, l, r);
} else {
plant_range(1, 0, L, l, r);
}
}
cout << sapling_cnt[1] << '\n' << removed_saplings << '\n';
return 0;
}复杂度
按线段树分析,整体复杂度可以看作:
空间复杂度:
总结
这题的重点不是普通区间和,而是:
- 区间状态覆盖
- 同时统计空地和树苗
一旦把位置状态拆清楚,线段树维护就很自然了。