不愧是大常数选手,无论原题还是加强都要开 O2
才能过去 TAT
Statement
给定一个长度为 (n) 的序列,求前 (k) 大的区间异或和。 (1leq nleq 5e5,1leq kleq min(frac{n(n-1)}{2},2e5),0leq a_i<2^{32})
Solution
虽然是十二省联考,但是题不难,也不是很毒瘤。
考虑区间异或和显然可以转化成两个前缀和的异或,那么现在的问题就变成了求前 (k) 大的异或对。由于这里是有序对,我们可以在开头把 (k imes 2) ,最后再让 (ans/=2) ,这样就可以直接统计无序点对了。
题目中有 (a_i<2^{32}) 的限制,容易想到用 Trie 来处理二进制数位。
首先,对于每个右端点,将使异或和取到最大值的左端点位置+异或和放进堆里。
然后,每次显然是取堆顶的一对。考虑如何在取出之后放入该右端点的次大值。一个显然的想法是把之前的删除,但是这样时空和实现都很麻烦。
考虑一个更简单的做法:其实 Trie 上不一定只能查询最值!我们也可以在 Trie 上用类似平衡树查询第 (p) 大的思想来找特定串的第 (k) 大异或和。
具体做法:在向 Trie 中插入的时候顺便统计子树大小。
对于一个原串在 Trie 上走到了 (g[pos][ch]) 的点,找最大值的时候是往 (g[pos][choplus 1]) 走,由于是从高位到低位,所以节点 (g[pos][choplus 1]) 子树中的任何串,和原串的异或和都比 (g[pos][ch]) 中的串要大。
那么如果当前要求第 (p) 大的异或和,而 (siz[g[pos][choplus 1]]<p) ,显然答案应该在 (g[pos][ch]) 中,那么 (pos-=siz[g[pos][choplus 1]]) ,然后向 (g[pos][ch]) 走;
否则加上当前位的贡献 (|=1<<i) ,向 (g[pos][choplus 1]) 走。代码如下:
if ( siz[g[pos][ch^1]]<p ) p-=siz[g[pos][ch^1]],pos=g[pos][ch];
else res|=1ll<<i,pos=g[pos][ch^1];
这样就只需要记录所有右端点当前用到了第几大 (rk[i]) ,在弹出一个右端点的时候 (rk[i]++) ,并查询对应 (rk[i]) 的异或和,插回堆里面即可。
Code
//Author:RingweEH
const int N=500010,M=N*35;
struct Node
{
int pos; ll val;
Node ( int _pos=0,ll _val=0 ) : pos(_pos),val(_val) {}
bool operator < ( const Node &tmp ) const { return val<tmp.val; }
};
priority_queue<Node> q;
int n,k,tot,rk[N],g[M][2],siz[M];
ll ans,a[N],b[N];
void Insert( ll x )
{
int pos=0;
for ( int i=33; ~i; i-- )
{
int ch=(x>>i)&1;
if ( !g[pos][ch] ) g[pos][ch]=++tot;
pos=g[pos][ch]; siz[pos]++;
}
}
ll Query( ll x,int p )
{
int pos=0; ll res=0;
for ( int i=33; ~i; i-- )
{
int ch=(x>>i)&1;
if ( siz[g[pos][ch^1]]<p ) p-=siz[g[pos][ch^1]],pos=g[pos][ch];
else res|=1ll<<i,pos=g[pos][ch^1];
}
return res;
}
int main()
{
n=read(); k=read(); k<<=1;
Insert( 0 );
b[0]=0;
for ( int i=1; i<=n; i++ )
{
a[i]=read(); b[i]=b[i-1]^a[i];
Insert( b[i] );
}
for ( int i=0; i<=n; i++ )
{
ll tmp=Query(b[i],rk[i]=1);
q.push( Node(i,tmp) );
}
ll ans=0;
while ( k-- )
{
ans+=q.top().val; int p=q.top().pos; q.pop();
ll tmp=Query(b[p],++rk[p]);
q.push( Node(p,tmp) );
}
printf( "%lld
",ans>>1 );
return 0;
}
加强版
Orz Apocrypha 为什么它也出现在了我们的模拟赛里
范围:(nleq 7.5*10^5,kleq frac{n*(n+1)}{2},a_i<2^{32}) . 于是需要一个 (Omicron(nlog n)) 的优秀做法。
Solution
同样做一遍异或前缀和,记为 (b) ,将题目转化为点对异或。有序对和无序对的处理同上。
考虑到 (K) 很大,显然不能枚举,那么可以枚举每一位计算贡献。大致思路就是,找出异或值第 (K) 大的数,然后求所有大于这个数的异或值之和。在 Trie 上找第 (K) 大的思路同上,但是中间要多统计一些东西:
同原题一样,首先我们要记录一个 (siz) 表示 Trie 树上节点的子树大小(注意是有效的结束节点个数),统计方法同上。
设当前节点为 (pos) ,当前这一位的值是 (ch) ,那么只有在 (g[pos][choplus 1]) 这个节点的子树中的 (b) 才能在这一位上产生贡献。
如果出现 (siz[g[pos][choplus 1]]<K) ,即上文需要减去 (siz) 的情况,再在 (g[pos][choplus 1]) 处打一个 (b[i]) 的标记,表示子树中所有的 (b[j]oplus b[i]) 都会对答案产生贡献,记录 (ans[bit]+=siz[g[pos][choplus 1]]) ,表示二进制下这一位上 (1) 的个数。
否则,(ans[bit]+=K) 。
这样做完之后,再 DFS 一遍,处理每个标记,然后加入答案的贡献即可,或者直接顺带统计也行。但是在统计答案的时候,处理标记仍然需要拆位,复杂度 (Omicron(nlog ^2n)) ,还需要优化。
考虑打标记是什么形式。能够在某个点上打上标记的数的集合一定都在某一棵子树里面,那么标记就转化成了两棵子树中所有 (b[i]) 的两两异或和。仍然拆位维护,统计子树中某一位上 (1) 的个数,在插入之前将 (b) 排序,这样每个子树就对应一段区间,比较好处理。
复杂度之所以是 (nlog) 的,是因为标记个数不超过 (Omicron(n)) 个。标记只会在三度点上出现。因此,一个节点 (x) 至多只会和另外一个点打标记。那么就不需要去重。
Code
小范围(但是保证时间复杂度与 (K) 无关)的提交地址:CF241B
这里是正经大范围的代码:数据自己造XD
我要杀了出题人!不知道为什么我的程序在所有数相同的时候会 WA
,然后特判了还是过不去,结果发现就算是这样的部分分也要开 Int128
!骗个分容易吗。
//Author:RingweEH
#define Int128 __int128
const int N=750010,M=32;
int n,tot=1,g[N*M][2],siz[N*M],bit[N][M],nw[N];
ll lim,a[N],m,cnt;
Int128 sum;
void Insert( int x )
{
int pos=1;
for ( int i=31; ~i; i-- )
{
int ch=x>>i&1;
if ( !g[pos][ch] ) g[pos][ch]=++tot;
pos=g[pos][ch]; siz[pos]++;
}
}
void GetLim()
{
ll las=m;
for ( int i=1; i<=n; i++ )
nw[i]=1;
for ( int i=31; ~i; i-- )
{
ll tmp=0;
for ( int j=1; j<=n; j++ )
{
int ch=a[j]>>i&1;
tmp+=siz[g[nw[j]][ch^1]];
}
if ( tmp>=las ) lim|=(1ll<<i);
else las-=tmp;
for ( int j=1; j<=n; j++ )
{
int ch=(a[j]^lim)>>i&1;
nw[j]=g[nw[j]][ch];
}
}
}
void GetSum()
{
for ( int i=31; ~i; i-- )
{
if ( lim>>i&1 ) continue;
ll now=((lim>>i)<<i)|(1ll<<i);
for ( int j=1; j<=n; j++ )
{
ll l=((a[j]^now)>>i)<<i,r=l|((1ll<<i)-1);
l=lower_bound( a+1,a+1+n,l )-a;
r=upper_bound( a+1,a+1+n,r )-a;
sum+=now*(r-l);
cnt+=r-l;
if ( l<=j && j<r ) cnt--;
for ( int k=i-1; ~k; k-- )
{
int tmp=bit[l][k]-bit[r][k];
if ( (a[j]>>k)&1 ) tmp=r-l-tmp;
sum+=(1ll<<k)*tmp;
}
}
}
}
void print( Int128 x)
{
if ( !x ) return;
print(x/10);
putchar( (char)(x%10+'0') );
}
void Same()
{
int cnt=0; ll num=0;
for ( int i=1; i<=n; i++ )
if ( a[i]==0 ) num+=(i-1)-cnt,cnt++;
else num+=cnt;
m>>=1;
Int128 res=a[2],mul=min( m,num );
print( res*mul );
}
int main()
{
n=read()+1; m=read()<<1; a[1]=0;
bool fl=1; ll a1;
for ( int i=2; i<=n; i++ )
{
ll x=read(); a[i]=x^a[i-1];
if ( i==2 ) a1=x;
else fl&=(a1==x);
}
if ( fl ) { Same(); return 0; }
sort( a+1,a+1+n );
for ( int i=n; i; i-- )
{
Insert( a[i] );
for ( int j=0; j<32; j++ )
bit[i][j]=bit[i+1][j]+((a[i]>>j)&1);
}
GetLim(); GetSum();
sum>>=1; cnt>>=1; m>>=1;
Int128 ans=(sum+(m-cnt)*lim);
print( ans );
return 0;
}