[USACO24DEC] Deforestation S(包括正确性证明)
更坏的阅读体验:我的CSDN
更新日志
2025/8/29 修改了正确性证明,使其更加直观。
题目传送门
题目大意
有一条数轴,上面有一些树,坐标为 \(a_1,a_2,\cdots,a_n\) 。现在给你 \(k\) 个限制,对于第 \(i\) 个限制,要求 \([l_i,r_i]\) 上必须至少有 \(t_i\) 颗树。
部分分思路
首先排序 \(a\),然后将限制按左端点排序。显然对于第 \(i\) 个限制,保留右边的 \(t_i\) 颗树是最不劣的 直观理解无需证明。
那么,我们遍历这个限制列表,然后从右端点开始添加树。时间复杂度 \(\text{O(NK)}\)。
伪代码实现:
#include <bits/stdc++.h>
using namespace std;
#define all(x) x.begin(), x.end()
typedef pair<int, int> pii;
const int N = 1e5 + 5;
int n, k;
void solve()
{
cin >> n >> k;
vector<int> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
sort(all(a));
vector<CONSTRAINTS> constraint(k);
vector<bool> planted(n, 0);
int ans = 0;
for (auto i: constraint) {
int cnt = 0;
for (int j = n; j; --j) {
if (i.l <= a[j] && a[j] <= i.r && planted[j]) {
++cnt;
}
}
for (int j = n; j; --j) {
if (cnt >= i.t) break;
if (i.l <= a[j] && a[j] <= i.r && !planted[j]) {
planted[j] = 1;
++cnt;
++ans;
}
}
}
cout << ans << '\n';
}
int main()
{
// ios::sync_with_stdio(false);
// cin.tie(nullptr);
int T;
cin >> T;
while (T--) solve();
return 0;
}
部分分优化(优先队列,满分)
考虑将树和约束放在一起考虑。
按 约束的左端点 / 树的坐标 为第一关键字排序,约束=0/树=1为第二关键字排序。
此外,对于一个约束,再多记录它的右端点和最大伐木数(区间 \([l_i,r_i]\) 内树数量减去需要保留的数量)。
再维护一个优先队列 constraint ,记录对于某个约束 \(i\) ,数轴上 \([1,r_i]\) 里最优可以砍掉的树木数量,用一个 pair<int, int> 表示即可,比较函数为以 \(r_i\) 为第一关键字(从小到大),最大树木砍伐数量(从小到大)为第二关键字进行排序。
同时,维护当前砍掉的树の数量ans。
每次若枚举到约束,则贪心地将约束区间内能砍的树数量全部砍掉并扔到 constraint 里,即 constraint.push({ans + cut_down, r});
若枚举到位置为 \(x\) 的树,则首先检查优先队列里的约束,其中 \(r<x\) 的,显然都不对目前及以后的任何树造成影响,因此可以直接 pop() 掉。在剩下的 constraint 里,若为空,则没有能对这棵树造成威胁的约束了,砍掉,++ans。否则若 constraint.top() 的最大伐木数量大于当前的 ans,即 ans < constraint.top().first ,同样 ++ans。
AC代码(不要走,后面有采但)
#include <bits/stdc++.h>
using namespace std;
#define all(x) x.begin(), x.end()
typedef pair<int, int> pii;
const int N = 1e5 + 5;
int n, k;
void solve()
{
cin >> n >> k;
vector<int> a(n);
for (int i = 0; i < n; ++i) {
cin >> a[i];
}
sort(all(a));
// l/coord
// type: 0 constrant; 1 tree
// r
// maximum amount of tree that can be cut down
vector<pair<pii, pii>> event;
for (auto i: a) {
event.push_back({{i, 1}, {0, 0}});
}
for (int i = 1; i <= k; ++i) {
int l, r, t;
cin >> l >> r >> t;
event.push_back({{l, 0}, {r, upper_bound(all(a), r) - lower_bound(all(a), l) - t}});
}
sort(all(event));
int ans = 0;
// // 用优先队列记录最有约束性的约束
// 记录对于当前枚举到的树的约束条件
priority_queue<pii, vector<pii>, greater<pii>> constraint;
for (auto i: event) {
int type = i.first.second;
if (type == 0) { // a constraint
int l = i.first.first, r = i.second.first, t = i.second.second;
// 显然当我们遇到一个限制时, 当前砍掉的树数量一定是[1,r]最优的
constraint.push({ans + t, r});
}
else { // a tree
int pos = i.first.first;
while (constraint.size()) {
if (constraint.top().second >= pos) break;
constraint.pop();
}
ans += constraint.empty() || constraint.top().first > ans;
}
}
cout << ans << '\n';
}
int main()
{
// freopen("deforestation.in", "r", stdin);
// freopen("deforestation.out", "w", stdout);
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--) solve();
return 0;
}
正确性证明
如此微妙的思路,当然需要一篇详细的证明,不要像某谷题解,第一篇就有一个小天才大蒟蒻的我发了占总评论数75%的问询评论。(这只是道绿题啊,然后我看题解看了4h没看懂,一定是题解我的语文问题awa)
后来去看 USACO 官方题解,
还是

正文:
证:
一些显然的引理:
引理0:ans表示的是-1e9到截止目前最后一颗处理的树的坐标内最多砍树数 (由其只在处理树时更新的计算方法可得)
引理1:处理一个区间时,ans所表示坐标必然小于目前区间左端点 \(l\) (由初始化时的排序可得)
引理2:处理一颗树时,所有囊括其的区间必然全部按右端点从左到右在 constraint(下文简称 q) 里 (由初始化时的排序可得)
由引理1,显然
q.emplace({ans + p, r});
这步操作中,ans+p 并不会造成区间i-1 和 区间i 重叠部分的树的重复计数,因为
- 首先
ans根本就没表示一个区间的右端点 - 其次由引理1
ans没有重叠
接下来我们讨论,为什么 q 按右端点从左到右排序一定是正确的,即为什么这样排在优先队列最前面的,右端点最左 的前提下 其截止此区间可砍伐树木数量最小的 约束是最“紧迫”,需要优先考虑的?
我们称右端点最左的限制为 \(A\) ,其后的限制为 \(B\)。\(A\) 之所以 “最紧迫”,是因为:
- 其有效范围(\(≤r_A\))最早结束,一旦超过 \(r_A\),无法用后续树弥补其保留要求,若不优先处理会直接违反限制。
- 优先满足 \(A\) 能让更靠右的树(同时属于 \(A\) 和 \(B\))保留下来,为后续限制(如 \(B\))留出更多砍伐空间,最大化总砍伐数。
使用例子证明紧迫性的约束的必要的
要理解“右端点最左的限制是最紧迫的”,以及违反它的后果,我们可以通过一个具体反例来拆解——先构造场景,再对比“优先满足紧迫限制”和“违反紧迫限制”两种选择的最终结果,从而直观看到后者的问题。
1. 树的信息(按坐标从小到大排序)
- 树A:坐标
x=2 - 树B:坐标
x=5
2. 限制的信息(按左端点排序,且左端点均≤2,保证能覆盖树A)
- 限制1(右端点更左,即“紧迫限制”):
区间[1,3],最多能砍p=1棵树(意思是:在[1,3]里砍树不能超过1棵)。 - 限制2(右端点更右,非紧迫):
区间[1,6],最多能砍p=2棵树(意思是:在[1,6]里砍树不能超过2棵)。
3. 事件排序(按“坐标/左端点”升序,限制优先于树)
根据算法的事件规则:
- 限制的“排序键”是左端点(均为1),树的“排序键”是坐标(2和5);
- 同排序键下,限制优先于树。
因此事件顺序为:
限制1 → 限制2 → 树A(x=2) → 树B(x=5)
“优先满足紧迫的限制”的正确选择
算法的优先队列(小根堆)以“右端点”为第一关键字,因此处理树A时,堆中先弹出右端点更左的限制1(而非限制2),决策过程如下:
- 处理限制1:堆中加入
(ans + p, r) = (0 + 1, 3)(ans初始为0)。 - 处理限制2:堆中加入
(0 + 2, 6),此时堆顶是限制1(右端点3 < 6)。 - 处理树
A(x=2):- 检查堆顶(限制1):右端点3 ≥ 2,有效。
- 限制1的
ans+p=1> 当前ans=0,说明可砍树A(砍后ans=1)。
- 处理树
B(x=5):- 先弹出堆顶(限制1的右端点3 < 5,无效),此时堆顶是限制2。
- 限制2的
ans+p=2> 当前ans=1,说明可砍树B(砍后ans=2)。
最终结果:砍2棵树,同时满足两个限制(限制1砍1棵,限制2砍2棵)。
“不优先满足紧迫的限制”的后果
假设我们“不优先处理右端点最左的限制1”,反而先处理限制2,会发生什么?
- 处理限制1和限制2后,堆中仍有两个限制,但我们刻意忽略堆顶的限制1,直接用限制2判断树A:
- 限制2的
ans+p=2> 当前ans=0,于是砍树A(ans=1)。
- 限制2的
- 此时,我们违反了限制1:限制1要求
[1,3]最多砍1棵树,而树A在[1,3]内,砍树A本身是允许的(没超1棵),但问题在后续——如果有第三棵树C(x=3,也在[1,3]内):- 处理树C时,若仍忽略限制1,用限制2判断:限制2的
ans+p=2> 当前ans=1,会继续砍树C(ans=2)。 - 此时限制1被打破(
[1,3]内砍了2棵树,超过p=1的上限),导致结果非法。
- 处理树C时,若仍忽略限制1,用限制2判断:限制2的
关键:为什么“违反紧迫限制”必然导致问题?
右端点最左的限制(如限制1 [1,3])有一个核心特点:它的“管辖范围”最小,且没有后续扩展的可能。
- 对于右端点更右的限制(如限制2
[1,6]),即使当前没处理,后续的树(如x=5、x=6)仍在其范围内,有“补救”的机会; - 但对于右端点最左的限制(如限制1
[1,3]),一旦错过其范围内的树(如x=2、x=3),后续再也没有树属于这个区间——如果此时超了限制的砍树数量,没有任何办法修正(因为无法“撤销”已砍的树),直接导致结果非法。
反证法:若不优先处理紧迫限制,必存在非法情况
假设存在一种最优方案,没有优先处理右端点最左的限制R(即处理R范围内的树时,先用了其他右端点更右的限制R')。
- 由于R的右端点 < R'的右端点,R的管辖范围是R'的子集(如R
[1,3]是R'[1,6]的子集)。 - 若在R的范围内砍树数量超过了R的p值(因为没优先用R的限制控制),则无论后续如何处理R',R的限制都已被打破——该方案非法。
- 若在R的范围内砍树数量未超R的p值,那么“优先用R”和“先用R'”的结果一致(砍树数量相同),但“优先用R”能更早排除无效限制(避免后续误判),不会比“先用R'”差。
因此,“不优先处理紧迫限制”要么导致非法,要么不优于“优先处理”,即优先处理右端点最左的限制是唯一正确的选择。
总结
违反右端点最左的限制,本质是“用范围更大的限制替代范围更小的限制”,会导致:
- 小范围限制的砍树数量失控(因为没有及时用其p值约束);
- 最终结果可能违反小范围限制的规则,变得非法;
- 即使暂时没失控,也无法获得比“优先处理紧迫限制”更优的结果。
这就是为什么右端点最左的限制是“最紧迫”的,必须优先处理。

浙公网安备 33010602011771号