校门外的树(增强版)

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

用线段树维护区间里空地数与树苗数,把状态分成老树、空地、树苗三类,就能同时处理整段砍树、局部补种和树苗损失统计。

OJ: luogu

题目 ID: P1276

难度:普及/提高-

标签:线段树区间模拟推导

日期: 2026-06-21 01:35

题意

从位置 0L,每个位置一开始都有一棵老树。

有两种操作:

  • 0 A B:把区间 [A, B] 上所有树都砍掉
  • 1 C D:把区间 [C, D] 上所有空地补种成树苗

最后要输出:

  1. 最终剩下多少棵树苗
  2. 曾经种上、后来又被砍掉的树苗有多少棵

思路

先看一个可以直接验证想法的朴素解:

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 直接按位置维护三种状态:

  • 老树
  • 空地
  • 树苗

每次操作逐点更新即可。

这题数据范围其实不大,直接模拟也能通过。

不过如果把它当作线段树练习题,会更有价值。

关键在于把每个位置的状态分成三类:

  • 老树
  • 空地
  • 树苗

在线段树的每个区间上维护:

  • 这段里有多少空地
  • 这段里有多少树苗

然后分类处理:

  1. 砍树操作
    • 整段都会变成空地
    • 如果这段里原本有树苗,它们都会被砍掉,所以要先累计到答案里
  2. 补种操作
    • 只有空地才能变成树苗
    • 如果某段已经没有空地,可以直接跳过
    • 如果某段全是空地,可以整段直接改成树苗

实现上再配一个区间统一状态标记,就能完成这些覆盖操作。

代码

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;
}

复杂度

按线段树分析,整体复杂度可以看作:

O(NlogL)O(N log L)

空间复杂度:

O(L)O(L)

总结

这题的重点不是普通区间和,而是:

  • 区间状态覆盖
  • 同时统计空地和树苗

一旦把位置状态拆清楚,线段树维护就很自然了。