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!