P8272
[USACO22OPEN] Apple Catching G
题目描述
天上下苹果了!在某些时刻,一定数量的苹果会落到数轴上。在某些时刻,Farmer John 的一些奶牛将到达数轴并开始接苹果。
如果一个苹果在没有奶牛接住的情况下落到数轴上,它就会永远消失。如果一头奶牛和一个苹果同时到达,奶牛就会接住苹果。每头奶牛每秒可以移动一单位距离。一旦一头奶牛接住了一个苹果,她就会离开数轴。
如果 FJ 的奶牛以最优方式合作,她们总共能接住多少个苹果?
输入格式
输入的第一行包含 \(N\)(\(1\le N\le 2\cdot 10^5\)),为苹果落到数轴上的次数或 FJ 的奶牛出现的次数。
以下 \(N\) 行每行包含四个整数 \(q_i\),\(t_i\),\(x_i\) 和 \(n_i\)(\(q_i\in \{1,2\}, 0\le t_i\le 10^9, 0\le x_i\le 10^9, 1\le n_i\le 10^3\))。
- 如果 \(q_i=1\),意味着 FJ 的 \(n_i\) 头奶牛在 \(t_i\) 时刻来到数轴上的 \(x_i\) 位置。
- 如果 \(q_i=2\),意味着 \(n_i\) 个苹果在 \(t_i\) 时刻落到了数轴上的 \(x_i\) 位置。
输入保证所有有序对 \((t_i,x_i)\) 各不相同。
模拟赛被创死,展现出普及组选手 cfm 强大的普及水准。
什么时候 \(i\) 可以接住苹果 \(a\)?(\(t_i \ge t_a\))
- 当 \(x_i \ge x_a\) 时,有 \(t_i - t_a \ge x_i - x_a\)。\(t_i - x_i \ge t_a - x_a\)。
- 当 \(x_i < x_a\) 时,有 \(t_i - t_a \ge x_a - x_i\)。\(t_i + x_i \ge t_a + x_a\)。
赛时的时候列出了后面两个式子,但是仍然并不会这道题。
我们注意到后面两个式子一定同时成立!这一点在移向前的式子非常明显,因为 \(t_i - t_ a\ge |x_i - x_a|\)。但是移向后似乎就没有这么显然了(upd:其实也非常显然,上下两式相加得到 \(t_i \ge t_a\)……,反过来只要减一下就好了……)
因此令 \(t_i - x_i = x_i, t_i + x_i = y_i\),那么 \(i\) 可以接住苹果 \(a\) 的充要条件就是 \(x_i \ge x_a, y_i \ge y_a\)。(这里的 \(x\) 和上面的 \(x\) 不是一样的!)这是一个二维偏序问题,但一般的二维偏序是计数,这里是最优化:将数对匹配。
回顾二维偏序的过程,是先对 \(x\) 进行排序,然后利用数据结构维护 \(y\)。这里我们也按照 \(x\) 排序以此消除 \(x_i \ge x_a\) 的约束。对于每个奶牛 \(i\) 考虑把能吃掉的苹果放到集合 \(S\) 中。从 \(x\) 小的奶牛往 \(x\) 大的奶牛考虑。不考虑删除时这个 \(S\) 是在增长的。接下来考虑删除,显然 \(y\) 越小的苹果越难被匹配。所以从 \(y\) 尽量小的奶牛开始匹配。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5;
struct node {
int opt, x, y, num;
bool operator < (const node &other) const {
return y < other.y;
}
} Q[N + 10];
bool cmp(node &x, node &y) {
return x.x > y.x;
}
multiset <node> S;
int n;
vector <node> vec[3];
int main() {
cin >> n;
for(int i = 1, t, x; i <= n; i++) {
cin >> Q[i].opt >> t >> x >> Q[i].num;
Q[i].x = t - x, Q[i].y = t + x;
vec[Q[i].opt].push_back(Q[i]);
}
sort(vec[1].begin(), vec[1].end(), cmp);
sort(vec[2].begin(), vec[2].end(), cmp);
int ia = 0, sum = 0;
for(int i = 0; i < vec[1].size(); i++) {
while(ia < vec[2].size() &&
vec[2][ia].x >= vec[1][i].x) {
S.insert(vec[2][ia]);
ia++;
}
while(1) {
if(!vec[1][i].num) break;
set <node>::iterator it = S.lower_bound(vec[1][i]);
if(it == S.end()) break;
node tmp = (*it);
if(tmp.num >= vec[1][i].num) {
sum += vec[1][i].num;
tmp.num -= vec[1][i].num;
vec[1][i].num = 0;
S.erase(it);
S.insert(tmp);
break;
}
else {
sum += tmp.num;
vec[1][i].num -= tmp.num;
S.erase(it);
}
}
}
cout << sum << endl;
}
总结
真的是一个很值得反思反思的题目。
复盘过程
- 在得到充要条件的一步,因为一般来讲是要根据 \(x\) 的大小讨论一下的。因此少前置约束是一个值得的动机。
- 从另一个角度,以少前置约束出发就是 \(t_i - t_a \ge |x_i - x_a|\),等价于 \(t_i - t_a \ge x_i - x_a, t_i - t_a \ge x_a - x_i\) 同时成立。即 \(x \ge |y|\) 等价于 \(x \ge y, x \ge -y\)。这个变形我之前似乎没有用过,记下来
- 于是得到了一个类二维偏序的匹配问题。对于一个高维问题,最常用的思想是降维。三位偏序二维偏序如此,高维前缀和如此,高维统计也如此。
- 高维问题
- 降维
- 独立维度
- 高维问题
- 接下来的贪心中,是从“难以匹配”的奶牛和苹果出发。这也启发对于匹配类的贪心问题应该从“难以匹配”的元素出发思考策略。
- 这是因为一般来说匹配问题不带权(比如这道题万一苹果带权似乎就不好贪心了)。难以匹配的赶紧匹配,容易匹配的放到后面匹配也没关系。这是“决策包容性”。
- 关于这块似乎还要结合几个线段贪心详细总结总结