更好的阅读体验:我的博客园
更新日志
2025/8/29 修改了正确性证明,使其更加直观。
题目传送门
题目大意
有一条数轴,上面有一些树,坐标为 a 1 , a 2 , ⋯ , a n a_1,a_2,\cdots,a_n a1,a2,⋯,an 。现在给你 k k k 个限制,对于第 i i i 个限制,要求 [ l i , r i ] [l_i,r_i] [li,ri] 上必须至少有 t i t_i ti 颗树。
部分分思路
首先排序
a
a
a,然后将限制按左端点排序。显然对于第
i
i
i 个限制,保留右边的
t
i
t_i
ti 颗树是最不劣的 直观理解无需证明。
那么,我们遍历这个限制列表,然后从右端点开始添加树。时间复杂度
O(NK)
\text{O(NK)}
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
]
[l_i,r_i]
[li,ri] 内树数量减去需要保留的数量)。
再维护一个优先队列 constraint ,记录对于某个约束
i
i
i ,数轴上
[
1
,
r
i
]
[1,r_i]
[1,ri] 里最优可以砍掉的树木数量,用一个 pair<int, int> 表示即可,比较函数为以
r
i
r_i
ri 为第一关键字(从小到大),最大树木砍伐数量(从小到大)为第二关键字进行排序。
同时,维护当前砍掉的树の数量ans。
每次若枚举到约束,则贪心地将约束区间内能砍的树数量全部砍掉并扔到 constraint 里,即 constraint.push({ans + cut_down, r});
若枚举到位置为
x
x
x 的树,则首先检查优先队列里的约束,其中
r
<
x
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
l
l (由初始化时的排序可得)
引理2:处理一颗树时,所有囊括其的区间必然全部按右端点从左到右在 constraint(下文简称 q) 里 (由初始化时的排序可得)
由引理1,显然
q.emplace({ans + p, r});
这步操作中,ans+p 并不会造成区间i-1 和 区间i 重叠部分的树的重复计数,因为
- 首先
ans根本就没表示一个区间的右端点 - 其次由引理1
ans没有重叠
接下来我们讨论,为什么 q 按右端点从左到右排序一定是正确的,即为什么这样排在优先队列最前面的,右端点最左 的前提下 其截止此区间可砍伐树木数量最小的 约束是最“紧迫”,需要优先考虑的?
我们称右端点最左的限制为
A
A
A ,其后的限制为
B
B
B。
A
A
A 之所以 “最紧迫”,是因为:
- 其有效范围( ≤ r A ≤r_A ≤rA)最早结束,一旦超过 r A r_A rA,无法用后续树弥补其保留要求,若不优先处理会直接违反限制。
- 优先满足 A A A 能让更靠右的树(同时属于 A A A 和 B B B)保留下来,为后续限制(如 B B B)留出更多砍伐空间,最大化总砍伐数。
625



被折叠的 条评论
为什么被折叠?



