You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

6.7 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

##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;
}