POJ 2828 Buy Tickets
\(POJ\) \(2828\) \(Buy\) \(Tickets\)
一、题目大意
有\(n\)个人,依次给出这\(n\)个人进入队列时前面有多少人\(p[i]\),和它的权值\(v[i]\),求最终队列的权值序列。
二、解题思路
基本思路:以终为始,倒序枚举
上栗子:
4
0 20523
1 19243
1 3890
0 31492
初始状况如下:
对于 \(0\) \(31492\),放在\(0\)号后面,就是在 \(1\) 号位置:
然后把 \(31492\) 抽掉(也就是不再需要\(31492\),看不到它了,不统计它了):
接着放 \(1\) \(3890\),放在\(1\)号后面,应该填在 \(2\) 号位置:
然后把 \(3890\) 抽掉:
接着放 \(1\) \(19243\),应该填在 \(2\) 号位置:
然后把 \(19243\)抽掉:
接着放 \(0\) \(20523\),应该填在 \(1\) 号位置:
抽掉后列表为空,表明插完了,总结果如下:
\(Q\):为什么要倒序枚举、以终为始,而不是正序枚举呢?
\(A\):如果按正常思路正序枚举,随着人员的增多,前面已经排好位置的人,会因为后面人员的加入,而导致位置变化,这就算不准了。正难则反,我们尝试下以终为始试试:
如果倒序枚举,最后一个(第\(n\)个)进入队伍的人,他前面的\(p[n]\)个人是固定的,前面\(n-1\)个怎么折腾我不管,但我的位置是准的。结合上面的例子,就是\(31492\)是排在\(0\)的后面,也是占了\(1\)号位置。
随着\(31492\)的占领\(1\)号位置,它也就被排除掉了,以后也不再检查它了,这样的话,问题会越来越清晰。
随着倒序的不断进行,会确定下来所有人的前面人数,也就可以完成准确的位置记录。
\(Q\): 线段树中的\(tr[u].sum\)这个区间和是什么意思?为什么要初始化为\(1\)?
\(A\): 这个区间和与普通的区间和概念上是不一样的,我称之为 怪异计数区间和。 要搞清楚它有什么用处,首先要知道大框架我们是在倒序枚举的基础上使用线段树解决问题的。
比如第\(n\)个人员,它占了\(3\)号位置,到第\(n-1\)个人员时,它是不用管\(3\)号位置的。为什么呢?你想啊,第\(n\)号是后插进来的,在\(n-1\)个人时,它没插进来,\(n-1\)个人是不用(也没法)考虑\(3\)号位置的。
\(tr[u].sum\)的含义:当前区间\(tr[u].l\)~\(tr[u].r\)之间,可以使用的空位有多少个,初始值为\(1\),表示单一的叶子节点,是有一个空位置的。
之所以要维护这样一个 怪异的区间和,是因为一会要求修改某个点的值时:modify(1, p[i] + 1, v[i])
,需要进行分裂,来决策是向\(u=1\)的左侧走还是右侧走,\(p[i]+1\)如果小于左儿子的区间和,也就是\(p[i]+1\)的位置可以在左侧找到足够的空位,就到左侧去修改,否则去右侧。右侧修改时,还需要减去左侧的区间空位\(sum\)和。
处理办法:
线段树每个节点维护 \(1\),如果该位被抽掉了,就替换为 \(0\)。
\(Q\): modify(1, p[i] + 1, v[i]);
怎么理解?
\(A\): 当前人前面有\(p[i]\)个人,他自己就是\(p[i]+1\)的位置,是确定的,对这个位置进行值的修改为\(v[i]\)
这道题还是非常的经典,倒序枚举+线段树+怪异区间和~
三、实现代码
#include <iostream>
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 200010;
int n, res[N];
int p[N], v[N]; // 在p[i]的后面,v[i]:权值
struct Node {
int l, r;
int sum; // 此区间内空位置的数量
} tr[N << 2];
void pushup(int u) {
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
void build(int u, int l, int r) {
tr[u].l = l, tr[u].r = r;
if (l == r) {
tr[u].sum = 1; // 每个叶子节点初始化为1,表示此区间内空位置的数量是1
return;
}
int mid = (l + r) >> 1;
build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
pushup(u); // 叶子有初始值1,所以这里需要向上更新信息
}
// 此处的x,是指在第x个空白位置上,不是普通前缀和的第x个位置的概念
void modify(int u, int x, int v) {
if (tr[u].l == tr[u].r) {
tr[u].sum = 0; // 此点被占用了,区间内空位置数量修改为0
// 注意:因为是魔改的线段树区间和,所以tr[u].l=tr[u].r≠x
// x含义:第x个空白位置
res[tr[u].l] = v; // 用结果数组记录最后此位置上是v这个权值
return;
}
// 下面也是魔改的关键部分:
// 如果修改的位置在左侧(左侧空白位置数>=x),递归左子树
if (x <= tr[u << 1].sum)
modify(u << 1, x, v);
else // 如果在右侧,递归右子树,注意 x 减去 左侧的空白数量
modify(u << 1 | 1, x - tr[u << 1].sum, v);
// 更新父节点信息
pushup(u);
}
/*
参考答案:
77 33 69 51
31492 20523 3890 19243
*/
int main() {
#ifndef ONLINE_JUDGE
freopen("POJ2828.in", "r", stdin);
#endif
// 加快读入
ios::sync_with_stdio(false), cin.tie(0);
while (cin >> n) {
build(1, 1, n); // 构建一个叶子节点值为1的线段树,描述此区间内空位置的数量
// p[i]:在p[i]的后面,即p[i]+1的位置上
// v[i]: 权值
for (int i = 1; i <= n; i++) cin >> p[i] >> v[i];
// 倒序枚举,以终为始,这样才不会破坏每个人的位置相对信息概念
for (int i = n; i; i--) modify(1, p[i] + 1, v[i]);
// 输出
for (int i = 1; i <= n; i++) printf("%d ", res[i]);
puts("");
}
return 0;
}