1 条题解
-
0
「ROI 2021 Day2」砍树 题解
题目理解
有 棵树按坐标顺序排列,每棵树有位置 和高度 。砍伐树木时需要选择倒伏方向(左或右),且不能碰到其他树木或超出边界 。
对于每个查询区间 ,需要计算在该区间内能砍伐的最大树木数量。
关键观察
1. 倒伏约束分析
对于树 :
- 向左倒伏:需要 且 范围内没有其他树木
- 向右倒伏:需要 且 范围内没有其他树木
2. 砍伐顺序的重要性
砍伐顺序影响可行性:
- 如果先砍伐中间的树,可能为其他树创造倒伏空间
- 最优策略通常是从边界开始砍伐
3. 区间查询特性
对于区间 ,只有该区间内的树可以被砍伐,区间外的树必须保持完好。
算法思路
方法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; }复杂度分析
- 预处理:(二分查找)
- 每次查询:(简化版本)或 (优化版本)
- 总复杂度:
优化方向
- 使用稀疏表:预处理区间最值,快速判断倒伏可行性
- 离线处理:对所有查询排序,批量处理
- 更精细的DP:定义 表示区间 的最大砍伐数,使用记忆化搜索
总结
本题的关键在于:
- 预处理每棵树的倒伏边界
- 利用砍伐顺序的贪心性质
- 对于大规模数据,需要使用高效的数据结构
这种"预处理 + 区间查询"的模式在竞赛中很常见,需要根据数据规模选择合适的算法。
- 1
信息
- ID
- 4426
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 9
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者