题解:洛谷 P11453 Deforestation

【题目来源】

洛谷:P11453 [USACO24DEC] Deforestation S - 洛谷

【题目描述】

Farmer John 正在扩大他的农场!他已经找到了完美的位置——红黑森林,由数轴上的 \(N\) 棵树(\(1≤N≤10^5\))组成,第 \(i\) 棵树位于位置 \(x_i\)\(−10^9≤x_i≤10^9\))。

环境保护法限制了 Farmer John 可以砍伐哪些树来为他的农场腾出空间。有 \(K\) 个限制(\(1≤K≤10^5\)),规定在线段 \([l_i,r_i]\)(包含端点)中必须始终至少存在 \(t_i\) 棵树(\(−10^9≤l_i,r_i≤10^9\))。输入保证红黑森林初始时满足这些限制。

Farmer John 想要他的农场尽可能大。请帮助他计算他可以砍伐的树的最大数量,同时仍然满足所有限制!

【输入】

每个测试点包含 \(T\)\(1≤T≤10\))个独立的测试用例。输入保证一个测试点中的所有 \(N\) 之和以及 \(K\) 之和均不超过 \(3⋅10^5\)

输入的第一行包含 \(T\)。每个测试用例的格式如下:

  • 第一行包含整数 \(N\)\(K\)
  • 下一行包含 \(N\) 个整数 \(x_1,\cdots,x_N\)
  • 以下 \(K\) 行,每行包含三个空格分隔的整数 \(l_i\)\(r_i\)\(t_i\)

【输出】

对于每个测试用例,输出一行,包含一个整数,表示 Farmer John 可以砍伐的树的最大数量。

【输入样例】

3
7 1
8 4 10 1 2 6 7
2 9 3
7 2
8 4 10 1 2 6 7
2 9 3
1 10 1
7 2
8 4 10 1 2 6 7
2 9 3
1 10 4

【输出样例】

4
4
3

【核心思想】

  1. 问题分析:给定数轴上 \(N\) 棵树的位置 \(x_i\)\(K\) 个区间约束 \([l_i, r_i]\),要求每个区间内至少保留 \(t_i\) 棵树。求最多可砍伐的树的数量。本质上是差分约束 + 最长路问题:设 \(S_i\) 为排序后前 \(i\) 个点中保留的树的数量,将约束转化为不等式组,用 SPFA 求最长路得到 \(S_n\) 的最大值,最终答案为 \(N - S_n\)

  2. 算法选择

    • 差分约束建模:将问题转化为 \(S_i\) 的约束不等式系统,用图论方法求解
    • SPFA 求最长路:差分约束的解对应图中的最长路径,用 SPFA 处理可能存在负权边的情况
  3. 关键步骤

    • 读取数据:读入 \(T\) 组测试数据,每组读入 \(N, K\)\(N\) 个坐标 \(x_i\)
    • 排序坐标sort(x[1..N]),将离散坐标排序后转化为前缀问题
    • 建图
      • 基础约束\(i = 1\)\(N\)):
        • \(S_i - S_{i-1} \leq 1\)(最多保留 \(1\) 棵):建边 \(i \to i-1\),权值 \(-1\)
        • \(S_i \geq S_{i-1}\)(单调不减):建边 \(i-1 \to i\),权值 \(0\)
      • 区间约束:对每个 \([l, r, t]\)
        • lower_boundupper_bound 找到排序后对应的位置区间 \([L, R]\)
        • 约束 \(S_R - S_{L-1} \geq t\):建边 \(L-1 \to R\),权值 \(t\)
    • SPFA 求最长路:从顶点 \(0\) 出发,求到所有点的最长距离 \(dist[i]\)
    • 输出\(N - dist[N]\)(最多砍伐数 = 总数 \(-\) 最少保留数)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(T \cdot (N + K) \log N)\),排序 \(O(N \log N)\),SPFA 平均 \(O(N + K)\)
    • 空间复杂度:\(O(N + K)\),邻接表存储
  5. 差分约束的核心思想

    • 前缀和建模\(S_i\) 表示前 \(i\) 个点中保留的数量,将"区间至少保留 \(t\) 棵"转化为 \(S_R - S_{L-1} \geq t\)
    • 不等式转图论\(x_j - x_i \geq c\) 对应边 \(i \to j\) 权值 \(c\),求最长路;\(x_j - x_i \leq c\) 对应边 \(j \to i\) 权值 \(-c\)
    • 单调性约束\(0 \leq S_i - S_{i-1} \leq 1\) 保证每个点要么保留要么砍伐,且前缀和不减
    • 答案转换:差分约束求的是 \(S_n\) 的最大值(最多保留),题目要求最多砍伐,故为 \(N - S_n\)
    • 适用于区间覆盖、资源分配、前缀约束类问题

【算法标签】

普及+ #差分约束

【代码详解】

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

const int N = 100005, M = N * 3;  // 最大顶点数和边数
int T, n, k;                       // T: 测试用例数, n: 点数, k: 区间约束数
int x[N];                          // 点坐标数组
int h[N], e[M], ne[M], w[M], idx;  // 链式前向星存储图
int dist[N];                       // 最长距离数组
bool st[N];                        // 标记顶点是否在队列中

/**
 * 添加有向边
 * @param a 起点
 * @param b 终点
 * @param c 权重
 */
void add(int a, int b, int c)
{
    e[idx] = b;        // 边指向的顶点
    w[idx] = c;        // 边的权重
    ne[idx] = h[a];    // 指向原链表头
    h[a] = idx++;      // 更新头指针
}

/**
 * SPFA算法求最长路径
 * 从顶点0开始,计算到所有顶点的最长路径
 */
void spfa()
{
    // 初始化距离为负无穷
    memset(dist, -0x3f, sizeof(dist));
    memset(st, 0, sizeof(st));

    queue<int> q;  // SPFA队列
    q.push(0);     // 起点入队
    st[0] = true;  // 标记在队列中
    dist[0] = 0;   // 起点距离为0

    while (!q.empty())
    {
        int t = q.front();  // 取出队首
        q.pop();
        st[t] = false;      // 标记不在队列中

        // 遍历t的所有邻接边
        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];  // 邻接顶点

            // 松弛操作:求最长路径
            if (dist[j] < dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];  // 更新最长距离

                // 如果j不在队列中,入队
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
}

int main()
{
    // 输入测试用例数
    cin >> T;

    while (T--)
    {
        // 输入点数和区间约束数
        cin >> n >> k;

        // 初始化邻接表
        memset(h, -1, sizeof(h));
        idx = 0;

        // 输入n个点的坐标
        for (int i = 1; i <= n; i++)
        {
            cin >> x[i];
        }

        // 对点坐标排序
        sort(x + 1, x + n + 1);

        // 添加基础约束边
        // 顶点编号0~n,表示排序后的前缀
        for (int i = 1; i <= n; i++)
        {
            // 约束1: S_i - S_{i-1} ≤ 1
            // 即: S_{i-1} - S_i ≥ -1
            // 建边: i → i-1,权重-1
            add(i, i - 1, -1);

            // 约束2: S_i ≥ S_{i-1}
            // 即: S_i - S_{i-1} ≥ 0
            // 建边: i-1 → i,权重0
            add(i - 1, i, 0);
        }

        // 处理k个区间约束
        while (k--)
        {
            int l, r, t;
            cin >> l >> r >> t;

            int len = r - l + 1;  // 区间长度(未使用)

            // 在排序后的点数组中查找l和r对应的位置
            // lower_bound: 找到第一个≥l的点
            l = lower_bound(x + 1, x + n + 1, l) - x;

            // upper_bound: 找到第一个>r的点,然后-1得到最后一个≤r的点
            r = upper_bound(x + 1, x + n + 1, r) - x - 1;

            // 添加区间约束边
            // 如果l≤r,表示有满足条件的点
            // 约束: S_r - S_{l-1} ≥ t
            // 建边: l-1 → r,权重t
            add(l - 1, r, t);
        }

        // 执行SPFA算法求最长路径
        spfa();

        /**
         * 输出结果
         * 设S_i表示前i个点中被选中的点数
         * 我们求的是S_n的最大值
         * 但题目要求的是不被选中的点数
         * 所以结果为: n - S_n
         */
        cout << n - dist[n] << endl;
    }

    return 0;
}

【运行结果】

3
7 1
8 4 10 1 2 6 7
2 9 3
4
7 2
8 4 10 1 2 6 7
2 9 3
1 10 1
4
7 2
8 4 10 1 2 6 7
2 9 3
1 10 4
3
posted @ 2026-08-17 09:45  团爸讲算法  阅读(10)  评论(0)    收藏  举报