1 条题解

  • 0
    @ 2025-10-28 10:34:36

    「ROI 2021 Day2」砍树 题解

    题目理解

    nn 棵树按坐标顺序排列,每棵树有位置 xix_i 和高度 hih_i。砍伐树木时需要选择倒伏方向(左或右),且不能碰到其他树木或超出边界 [x1,xn][x_1, x_n]

    对于每个查询区间 [l,r][l, r],需要计算在该区间内能砍伐的最大树木数量。

    关键观察

    1. 倒伏约束分析

    对于树 ii

    • 向左倒伏:需要 xihix1x_i - h_i \geq x_1(xihi,xi)(x_i - h_i, x_i) 范围内没有其他树木
    • 向右倒伏:需要 xi+hixnx_i + h_i \leq x_n(xi,xi+hi)(x_i, x_i + h_i) 范围内没有其他树木

    2. 砍伐顺序的重要性

    砍伐顺序影响可行性:

    • 如果先砍伐中间的树,可能为其他树创造倒伏空间
    • 最优策略通常是从边界开始砍伐

    3. 区间查询特性

    对于区间 [l,r][l, r],只有该区间内的树可以被砍伐,区间外的树必须保持完好。

    算法思路

    方法1:动态规划 + 预处理(适用于小数据)

    步骤1:预处理每棵树的倒伏可行性

    #include <iostream>
    #include <vector>
    #include <algorithm>
    using namespace std;
    
    const int MAXN = 5005; // 对于子任务1-3
    
    int n, q;
    vector<long long> x, h;
    vector<int> left_ok, right_ok;
    
    void preprocess() {
        left_ok.resize(n + 1);
        right_ok.resize(n + 1);
        
        for (int i = 1; i <= n; i++) {
            // 检查向左倒伏
            left_ok[i] = 1;
            if (x[i] - h[i] < x[1]) {
                left_ok[i] = 0;
            } else {
                // 检查是否会碰到区间内的树
                for (int j = 1; j < i; j++) {
                    if (x[i] - h[i] < x[j] && x[j] < x[i]) {
                        left_ok[i] = 0;
                        break;
                    }
                }
            }
            
            // 检查向右倒伏
            right_ok[i] = 1;
            if (x[i] + h[i] > x[n]) {
                right_ok[i] = 0;
            } else {
                // 检查是否会碰到区间内的树
                for (int j = i + 1; j <= n; j++) {
                    if (x[i] < x[j] && x[j] < x[i] + h[i]) {
                        right_ok[i] = 0;
                        break;
                    }
                }
            }
        }
    }
    

    步骤2:区间DP计算最大砍伐数

    int solve_interval(int l, int r) {
        vector<vector<int>> dp(r - l + 2, vector<int>(r - l + 2, 0));
        
        for (int len = 1; len <= (r - l + 1); len++) {
            for (int i = l; i <= r - len + 1; i++) {
                int j = i + len - 1;
                
                // 尝试砍伐左边的树i
                if (left_ok[i] || (i == l && right_ok[i])) {
                    dp[i][j] = max(dp[i][j], 1 + dp[i + 1][j]);
                }
                
                // 尝试砍伐右边的树j
                if (right_ok[j] || (j == r && left_ok[j])) {
                    dp[i][j] = max(dp[i][j], 1 + dp[i][j - 1]);
                }
                
                // 不砍伐边界树木
                dp[i][j] = max(dp[i][j], dp[i + 1][j]);
                dp[i][j] = max(dp[i][j], dp[i][j - 1]);
            }
        }
        
        return dp[l][r];
    }
    

    方法2:贪心 + 线段树(适用于大数据)

    步骤1:预处理每棵树能倒伏的最远位置

    #include <bits/stdc++.h>
    using namespace std;
    
    const int MAXN = 500005;
    
    int n, q;
    long long x[MAXN], h[MAXN];
    int left_bound[MAXN], right_bound[MAXN];
    
    void precompute_bounds() {
        // 预处理每棵树向左倒伏能到达的最左位置
        for (int i = 1; i <= n; i++) {
            long long left_reach = x[i] - h[i];
            if (left_reach < x[1]) {
                left_bound[i] = 0; // 不能向左倒伏
            } else {
                // 二分查找第一个位置 >= left_reach
                int l = 1, r = i - 1, pos = 0;
                while (l <= r) {
                    int mid = (l + r) / 2;
                    if (x[mid] >= left_reach) {
                        pos = mid;
                        r = mid - 1;
                    } else {
                        l = mid + 1;
                    }
                }
                left_bound[i] = pos;
            }
        }
        
        // 预处理每棵树向右倒伏能到达的最右位置
        for (int i = 1; i <= n; i++) {
            long long right_reach = x[i] + h[i];
            if (right_reach > x[n]) {
                right_bound[i] = n + 1; // 不能向右倒伏
            } else {
                // 二分查找最后一个位置 <= right_reach
                int l = i + 1, r = n, pos = n + 1;
                while (l <= r) {
                    int mid = (l + r) / 2;
                    if (x[mid] <= right_reach) {
                        pos = mid;
                        l = mid + 1;
                    } else {
                        r = mid - 1;
                    }
                }
                right_bound[i] = pos;
            }
        }
    }
    

    步骤2:使用线段树维护区间信息

    struct SegmentTree {
        vector<int> tree;
        int size;
        
        SegmentTree(int n) {
            size = 1;
            while (size < n) size *= 2;
            tree.resize(2 * size, 0);
        }
        
        void update(int idx, int val) {
            idx += size;
            tree[idx] = val;
            for (idx /= 2; idx >= 1; idx /= 2) {
                tree[idx] = max(tree[2 * idx], tree[2 * idx + 1]);
            }
        }
        
        int query(int l, int r) {
            l += size;
            r += size;
            int res = 0;
            while (l <= r) {
                if (l % 2 == 1) res = max(res, tree[l++]);
                if (r % 2 == 0) res = max(res, tree[r--]);
                l /= 2;
                r /= 2;
            }
            return res;
        }
    };
    
    int solve_query(int l, int r) {
        // 贪心策略:从边界开始砍伐
        vector<bool> can_cut(n + 1, false);
        int count = 0;
        
        // 检查边界树木
        if (l == r) {
            // 单棵树:检查是否能倒向区间外
            if (left_bound[l] < l || right_bound[l] > r) {
                return 1;
            }
            return 0;
        }
        
        // 检查左边界树
        if (left_bound[l] < l || (left_bound[l] == 0 && right_bound[l] > r)) {
            can_cut[l] = true;
            count++;
        }
        
        // 检查右边界树
        if (right_bound[r] > r || (right_bound[r] == n + 1 && left_bound[r] < l)) {
            can_cut[r] = true;
            count++;
        }
        
        // 对于区间内部的树,需要更复杂的逻辑
        // 这里使用简化的贪心策略
        for (int i = l + 1; i < r; i++) {
            if ((left_bound[i] < l && right_bound[i] > r) ||
                (left_bound[i] == 0 && right_bound[i] > r) ||
                (right_bound[i] == n + 1 && left_bound[i] < l)) {
                can_cut[i] = true;
                count++;
            }
        }
        
        return count;
    }
    

    完整解决方案(适用于中等规模)

    #include <iostream>
    #include <vector>
    #include <algorithm>
    using namespace std;
    
    int main() {
        ios_base::sync_with_stdio(false);
        cin.tie(nullptr);
        
        int n, q;
        cin >> n >> q;
        
        vector<long long> x(n + 1), h(n + 1);
        for (int i = 1; i <= n; i++) {
            cin >> x[i] >> h[i];
        }
        
        // 预处理倒伏边界
        vector<int> left_bound(n + 1), right_bound(n + 1);
        for (int i = 1; i <= n; i++) {
            // 向左倒伏边界
            long long left_reach = x[i] - h[i];
            if (left_reach < x[1]) {
                left_bound[i] = 0;
            } else {
                auto it = lower_bound(x.begin() + 1, x.end(), left_reach);
                left_bound[i] = it - x.begin();
            }
            
            // 向右倒伏边界
            long long right_reach = x[i] + h[i];
            if (right_reach > x[n]) {
                right_bound[i] = n + 1;
            } else {
                auto it = upper_bound(x.begin() + 1, x.end(), right_reach);
                right_bound[i] = it - x.begin() - 1;
            }
        }
        
        // 处理查询
        while (q--) {
            int l, r;
            cin >> l >> r;
            
            if (l == r) {
                // 单棵树情况
                if (left_bound[l] < l || right_bound[l] > r) {
                    cout << 1 << "\n";
                } else {
                    cout << 0 << "\n";
                }
                continue;
            }
            
            int count = 0;
            // 检查边界树木
            if (left_bound[l] < l || (left_bound[l] == 0 && right_bound[l] > r)) {
                count++;
            }
            if (left_bound[r] < l || (right_bound[r] == n + 1 && left_bound[r] < l)) {
                count++;
            }
            
            // 检查内部树木(简化版本)
            for (int i = l + 1; i < r; i++) {
                if ((left_bound[i] < l && right_bound[i] > r) ||
                    (left_bound[i] == 0 && right_bound[i] > r) ||
                    (right_bound[i] == n + 1 && left_bound[i] < l)) {
                    count++;
                }
            }
            
            cout << count << "\n";
        }
        
        return 0;
    }
    

    复杂度分析

    • 预处理O(nlogn)O(n \log n)(二分查找)
    • 每次查询O(rl)O(r - l)(简化版本)或 O(logn)O(\log n)(优化版本)
    • 总复杂度O(nlogn+q平均区间长度)O(n \log n + q \cdot \text{平均区间长度})

    优化方向

    1. 使用稀疏表:预处理区间最值,快速判断倒伏可行性
    2. 离线处理:对所有查询排序,批量处理
    3. 更精细的DP:定义 dp[l][r]dp[l][r] 表示区间 [l,r][l, r] 的最大砍伐数,使用记忆化搜索

    总结

    本题的关键在于:

    1. 预处理每棵树的倒伏边界
    2. 利用砍伐顺序的贪心性质
    3. 对于大规模数据,需要使用高效的数据结构

    这种"预处理 + 区间查询"的模式在竞赛中很常见,需要根据数据规模选择合适的算法。

    • 1

    信息

    ID
    4426
    时间
    1000ms
    内存
    256MiB
    难度
    9
    标签
    递交数
    1
    已通过
    1
    上传者