[USACO24DEC] Deforestation S(包括正确性证明)

更坏的阅读体验:我的CSDN

更新日志

2025/8/29 修改了正确性证明,使其更加直观。

题目传送门

洛谷P11453

题目大意

有一条数轴,上面有一些树,坐标为 \(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 官方题解,
还是
懵逼了awa
正文:
证:
一些显然的引理:
引理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. 处理限制1:堆中加入 (ans + p, r) = (0 + 1, 3)(ans初始为0)。
  2. 处理限制2:堆中加入 (0 + 2, 6),此时堆顶是限制1(右端点3 < 6)。
  3. 处理树 A(x=2)
    • 检查堆顶(限制1):右端点3 ≥ 2,有效。
    • 限制1的 ans+p=1 > 当前 ans=0,说明可砍树A(砍后 ans=1)。
  4. 处理树 B(x=5)
    • 先弹出堆顶(限制1的右端点3 < 5,无效),此时堆顶是限制2。
    • 限制2的 ans+p=2 > 当前 ans=1,说明可砍树B(砍后 ans=2)。

最终结果:砍2棵树,同时满足两个限制(限制1砍1棵,限制2砍2棵)。

“不优先满足紧迫的限制”的后果

假设我们“不优先处理右端点最左的限制1”,反而先处理限制2,会发生什么?

  1. 处理限制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的上限),导致结果非法。

关键:为什么“违反紧迫限制”必然导致问题?

右端点最左的限制(如限制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'”差。

因此,“不优先处理紧迫限制”要么导致非法,要么不优于“优先处理”,即优先处理右端点最左的限制是唯一正确的选择

总结

违反右端点最左的限制,本质是“用范围更大的限制替代范围更小的限制”,会导致:

  1. 小范围限制的砍树数量失控(因为没有及时用其p值约束);
  2. 最终结果可能违反小范围限制的规则,变得非法;
  3. 即使暂时没失控,也无法获得比“优先处理紧迫限制”更优的结果。

这就是为什么右端点最左的限制是“最紧迫”的,必须优先处理。

posted @ 2025-08-29 15:46  peter_code  阅读(19)  评论(0)    收藏  举报