题目描述:
有一个多重集,初始为空。n次操作,每次往多重集插入一个整数,求每次插入完后多重集的中位数,输出所有中位数的异或和。
给出第一次操作的插入的数和
n
n
n,其余的数按某种方式随机生成。
50
%
50\%
50%的数据,
n
≤
1
0
6
n\le10^6
n≤106
100
%
100\%
100%的数据,
n
≤
3
∗
1
0
7
n\le3*10^7
n≤3∗107
Upd:LOJ上有:#6137. 「2017 山东三轮集训 Day4」Mid
题目分析:
最慢但是最无脑的做法:平衡树(splay)。
有点技巧但是卡不过100%的做法:最大堆和最小堆维护序列中位数
具体就是记录当前的中位数
m
i
d
mid
mid,和小于等于它的元素组成的最大堆,大于等于它的元素组成的最小堆,插入时跟mid比大小,然后根据两边堆的大小调整mid。需要一次插入和一次删除。
堆的这种做法也可以换成set,这样就可以直接在一个set里面迭代器移动。基本每次需要一次插入和一次移动。
因为序列是随机的,所以可以设一个阈值
l
i
m
lim
lim,当set的元素个数大于
l
i
m
lim
lim时每插入一个数就在对应的那一边去掉头或尾(当然这种做法如果在某一边插很多就会错),这样set的log就被降下来了(虽然会多一个删除操作),本地实测当
l
i
m
=
1
e
4
lim=1e4
lim=1e4时可以在
1.67
s
1.67s
1.67s内通过。
标程是在第一种堆的做法上利用随机继续优化,把中间的一个数换成一个数组(双端队列),同样设一个阈值
l
i
m
lim
lim,如果插入的数小于左边最大堆的堆顶就往左边插,大于右边最小堆的堆顶就往右边插,否则就往中间暴力插入(移动数组)。
插入完后如果中位数落在左边堆里,就将左边的堆弹出到数组的头部。
如果中位数落在右边堆里,就将右边的堆弹出到数组的尾部。
然后检查数组的大小是否超过
l
i
m
lim
lim,如果超过就看哪边堆小就往哪边放。
然后中位数就必然落在数组中了,直接返回即可。
当n较大后插入基本上会落在两边的堆中,由于序列随机,对比第一种做法,中间的数组相当于起到了缓冲的作用,所以调整次数相对减少。
平衡
s
2
+
n
/
s
s^2+n/s
s2+n/s,取
s
=
246
s=246
s=246理论复杂度最优,实际上取几百都差不多。本地测
1.6
s
1.6s
1.6s
Code(手写插入比push_heap快,手写删除却比pop_heap慢…):
#include<bits/stdc++.h>
using namespace std;
const int N = 15000005, S = 500, P = 1e9+7;
struct Heap{
int a[N],n; Heap(){a[0]=2e9;}
inline void push(int x){
int i=++n;
for(;x>a[i>>1];i>>=1) a[i]=a[i>>1];
a[i]=x;
}
inline void pop(){pop_heap(a+1,a+1+n),n--;}
inline int top(){return a[1];}
}L,R;
int q[N/S*2],l=N/S+1,r=N/S,cnt;
inline void force(int x){
int i=r;
for(;i>=l&&x<q[i];i--) q[i+1]=q[i];
q[i+1]=x,r++;
}
int ins(int x){
int k=(++cnt+1)>>1;
if(r-l<S) force(x);
else if(x<=q[l]) L.push(x);
else if(x>=q[r]) R.push(-x);
else {if(L.n<R.n) L.push(q[l++]); else R.push(-q[r--]); force(x);}
if(L.n>=k) q[--l]=L.top(),L.pop(),R.push(-q[r--]);
if(R.n>cnt-k) q[++r]=-R.top(),R.pop(),L.push(q[l++]);
return q[l+k-L.n-1];
}
int main()
{
freopen("mid.in","r",stdin);
freopen("mid.out","w",stdout);
int n,x,ans=0,last;
scanf("%d%d",&n,&x);
for(int i=1;i<=n;i++)
ans^=last=ins(x),x=(1714636915ll*x+1681692777)%P*(846930886ll*last%P+1804289383)%P;
printf("%d\n",ans);
}