POJ#2828-Buy Tickets-[线段树][块状链表][STL][错题]

  ACM的线段树复习已经接近尾声,这道题就作为此轮线段树复习的最后一道吧。
题目链接
题目大意
  插队问题。每次给出一个位置pos和一个编号val。表示编号为val的人想要插队到第pos个人的后面,默认第0位为售票亭。输出所有插队操作结束后的队列顺序。
数据范围
  插队操作 n<=200000,编号 val<=32767。其中第i个请求插队的人的pos值∈[0,i)。有多组数据。
题目分析
  这道题是我在一个线段树题集里面找到的,但初看此题,我怎么看都觉得是一道平衡树的模板题啊。或者块状链表,rope,pb_ds库里的名次树都可以搞搞。但是,那样做too young,too simple。只有线段树做法才能展现ACMer的姿势水平。
  言归正传。简单来说,线段树在这里充当的是一个查询前缀和的功能。因此,使用二分加树状数组理论上是可行的,事实上它也能通过测试。我们可以发现,后面插队的人优先级高于前面插队的人,而最后一个人插队的位置一定是可以被满足的,倒数第二个位置的人只需要除去最后一个人占的位置,再进行插队即可。以此类推,只要将插队的顺序倒过来,当第i个人插队的时候,排除掉已经占用的位置,再从前往后找到的第posi个空位就是他所在的位置了。因此,只需要一个能够进行单点修改,查询前K个空位的位置的数据结构即可。这里我采用的是线段树,细节说明详见代码注释。

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define pr pair<int,int>
#define mp make_pair
using namespace std;
const int MAXN=200010;
const int MAXM=40000;
int n;
int pos[MAXM],name[MAXN];
pr req[MAXN];
struct tree
{
    int sum[MAXN*4];
    void init()
    {
        init(1,1,n);
    }
    void init(int root,int L,int R)
    {
        sum[root]=R-L+1;
        if (L==R) return ;
        init(root<<1,L,L+R>>1);
        init(root<<1|1,(L+R>>1)+1,R);
    }
    int insert(int pos)
    {
        return insert(1,1,n,pos+1);
    }
    int insert(int root,int L,int R,int pos)//返回第pos个空位的绝对位置
    {
        sum[root]--;
        if (L==R) return L;
        int M=L+R>>1;
        if (sum[root<<1]>=pos) return insert(root<<1,L,M,pos);//当左边的空位数多于待查询的位置时,在左侧递归查询
        else return insert(root<<1|1,M+1,R,pos-sum[root<<1]);//当左侧空位不足,证明该位置在右侧。减去左侧的空位数即得在右儿子结点的待查询位置
    }
}T;
int main()
{
    while (~scanf("%d",&n))
    {
        T.init();
        memset(pos,0,sizeof pos);
        memset(name,-1,sizeof name);
        for (int i=1;i<=n;i++)
        {
            scanf("%d%d",&req[i].first,&req[i].second);
        }
        for (int i=n;i;i--)
        {
            pos[req[i].second]=T.insert(req[i].first);
            name[pos[req[i].second]]=req[i].second;
        }
        for (int i=1;i<=n;i++) if (~name[i]) printf("%d ",name[i]);
        printf("\n");
    }
    return 0;
}

  这样就可以顺利A掉这道题了。可是,且慢,题目明明说有200000次插队操作,但val值只有32767,那么必定有重复的存在!但是题目并没有说明如何处理这种情况。按照题意,每个插队的人都被赋予一个val值。那val值为k的人如果先后两次插队的话,应该是他先从原来站的位置走出来,然后插到他想插的位置。但很显然,如果按照这样理解的话,这种算法根本得不出正确答案。因为这不仅仅是插队问题,还有离队问题,而加上离队操作后,是无法通过倒序建树来进行的。因为按照题意来模拟的话,某时刻某人进行插队的时候,在这之前插队的人是要影响他插队的绝对位置的,而按照此算法,我们只能推知在该时间点之后的插队情况,某人在此之前是否离队,我们是不知道的,因此无法计算当前插队的人的位置。我参考了网上的AC代码,发现大家出奇一致地忽略了这个问题,直接将两个相同val值的人当作不同的两次插队操作,在最终序列里也会输出两次这个人。poj上的数据也应该是按照这种理解进行的。如此理解的话,那这种算法就是完美的了。
  当然这道题的解法很多,其中我觉得稍稍有点挑战的是复杂度为 O ( n n ) O(n\sqrt{n}) O(nn )的块状链表,需要压缩常数因子才能通过。我第一次尝试了块状链表的指针写法和内存申请与回收。这种算法很简单,就是从头开始模拟,加上一些基础的块链操作。至于块状链表的维护,我是在某一块大于某个值的时候就进行一次分裂操作,调整一些参数之后,再加上输入输出优化,能够通过测试。下面贴出参考代码。

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int MAXM=33000;
const int M=280,R=480;//块大小大于R时进行一次分裂
int n,p;
inline int getint()
{
    char ch;
    int p=0;
    while ((ch=getchar())<'0'||ch>'9');
    p*=10;p+=ch-'0';
    while ((ch=getchar())>='0'&&ch<='9') p*=10,p+=ch-'0';
    return p;
}
inline void outint(int k)
{
    char ch;
    int tot=0;
    char a[8];
    if (!k){putchar('0');return;}
    while (k){ch=k%10+'0';k/=10;a[++tot]=ch;}
    for (int i=tot;i>=1;i--) putchar(a[i]);
}
struct node
{
    int size;
    int num[R+5];
    node *pre,*next;
    node ()
    {
        memset(this,0,sizeof (node));
    }
    void init(int *st,int *ed)//批量加入块末端
    {
        for (;st<=ed;st++) {num[++size]=*st;}
    }
    void insert(int pos,int id)//单个暴力插入
    {
        for (int i=size++;i>=pos;i--) num[i+1]=num[i];
        num[pos]=id;
        if (size>R) split();
    }
    void split()
    {
        node *temp=new node;
        int mid=size>>1;
        temp->init(num+mid+1,num+size);
        size=mid;
        if (next) next->pre=temp;
        temp->next=next;
        next=temp;
    }
    void print()
    {
        for (int i=1;i<=size;i++) outint(num[i]),putchar(' ');
    }
}*root;
void jump(int pos,int val)//暴力查找插入的位置
{
    for (node *i=root;i;i=i->next)
    {
        if (pos<=i->size+1) {i->insert(pos,val);break;}
        else pos-=i->size;
    }
}
int main()
{
    root=new node;
    while (~scanf("%d",&n))
    {
        node* temp=root->next;
        while (true)
        {
            delete root;
            if (!temp) break;
            root=temp;
            temp=temp->next;
        }
        root=new node;
        for (int i=1;i<=n;i++)
        {
            int pos,val;
            pos=getint();val=getint();
            jump(pos+1,val);
        }
        for (node *i=root;i;i=i->next)
        {
            i->print();
        }
        printf("\n");
    }
    return 0;
}

  另外,还有两种使用STL但是很遗憾因为超时不能通过测试的算法供参考。

  理论复杂度 O ( n n ) O(n\sqrt{n}) O(nn )但常数十分巨大的rope版本。

#include <cstdio>
#include <cstring>
#include <ext/rope>
#include <algorithm>
using namespace std;
using namespace __gnu_cxx;
rope <int> Q;
int n;
int main()
{
    while (~scanf("%d",&n))
    {
        Q.clear();
        for (int i=1;i<=n;i++)
        {
            int pos,val;
            scanf("%d%d",&pos,&val);
            Q.insert(pos,val);
        }
        for (int i=0;i<Q.size();i++)
        {
            printf("%d ",Q[i]);
        }
        printf("\n");
    }
    return 0;
}

  复杂度 O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n)但常数稍大的pb_ds平衡树算法。(其实真相是偷懒不想手写splay,否则用平衡树也是有可能过的)

#include <cstdio>
#include <algorithm>
#include <bits/stdc++.h>
#include <ext/pb_ds/tree_policy.hpp>
#include <ext/pb_ds/assoc_container.hpp>
using namespace __gnu_pbds;
using namespace std;
inline int getint()
{
    char ch;
    int p=0;
    while ((ch=getchar())<'0'||ch>'9');
    p*=10;p+=ch-'0';
    while ((ch=getchar())>='0'&&ch<='9') p*=10,p+=ch-'0';
    return p;
}
inline void outint(int k)
{
    char ch;
    int tot=0;
    char a[8];
    if (!k){putchar('0');return;}
    while (k){ch=k%10+'0';k/=10;a[++tot]=ch;}
    for (int i=tot;i>=1;i--) putchar(a[i]);
}
struct node
{
    double val;
    int id;
    bool operator > (const node &A) const
    {
        return val<A.val;//注意这个小于号
    }
};
tree<node,null_type,greater<node>,rb_tree_tag,tree_order_statistics_node_update> T;
tree<node,null_type,greater<node>,rb_tree_tag,tree_order_statistics_node_update>::iterator iter,temp;
int n;
int main()
{
    while (~scanf("%d",&n))
    {
        T.clear();
        for (int i=1;i<=n;i++)
        {
            int pos,v;
            pos=getint();v=getint();
            if (T.empty())
                T.insert((node){0,v});
            else if (pos==0)//find_by_order(k)虽然是查找第k+1大的元素,但是不支持小于0的k,也就是查不了第0个数据,而且这个数据结构的第1个是第0大
                T.insert((node){T.begin()->val-100,v});//加减100来确定新插进去的端点的键值,减小精度爆炸的概率
            else
            {
                iter=T.find_by_order(pos-1);
                temp=iter;temp++;
                if (temp==T.end())
                    T.insert((node){iter->val+100,v});
                else
                    T.insert((node){(iter->val+temp->val)/2.0,v});//采用取前后浮点数取中的方式强行取键值插入,存在插入过密导致精度爆炸的bug
            }
        }
        for (iter=T.begin();iter!=T.end();iter++)
        {
            outint(iter->id);putchar(' ');
        }
        printf("\n");
    }
    return 0;
}

  还有像二分+树状数组splaytree,treap啥啥的都是可以搞搞的,等复习到那去了再写写这道题,似乎这题还卡splay的常数。就酱吧。byebye!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值