【题解】异或粽子&加强版
不愧是大常数选手,无论原题还是加强都要开 O2
才能过去 TAT
Statement
给定一个长度为 \(n\) 的序列,求前 \(k\) 大的区间异或和。 \(1\leq n\leq 5e5,1\leq k\leq \min(\frac{n(n-1)}{2},2e5),0\leq a_i<2^{32}\)
Solution
虽然是十二省联考,但是题不难,也不是很毒瘤。
考虑区间异或和显然可以转化成两个前缀和的异或,那么现在的问题就变成了求前 \(k\) 大的异或对。由于这里是有序对,我们可以在开头把 \(k\times 2\) ,最后再让 \(ans/=2\) ,这样就可以直接统计无序点对了。
题目中有 \(a_i<2^{32}\) 的限制,容易想到用 Trie 来处理二进制数位。
首先,对于每个右端点,将使异或和取到最大值的左端点位置+异或和放进堆里。
然后,每次显然是取堆顶的一对。考虑如何在取出之后放入该右端点的次大值。一个显然的想法是把之前的删除,但是这样时空和实现都很麻烦。
考虑一个更简单的做法:其实 Trie 上不一定只能查询最值!我们也可以在 Trie 上用类似平衡树查询第 \(p\) 大的思想来找特定串的第 \(k\) 大异或和。
具体做法:在向 Trie 中插入的时候顺便统计子树大小。
对于一个原串在 Trie 上走到了 \(g[pos][ch]\) 的点,找最大值的时候是往 \(g[pos][ch\oplus 1]\) 走,由于是从高位到低位,所以节点 \(g[pos][ch\oplus 1]\) 子树中的任何串,和原串的异或和都比 \(g[pos][ch]\) 中的串要大。
那么如果当前要求第 \(p\) 大的异或和,而 \(siz[g[pos][ch\oplus 1]]<p\) ,显然答案应该在 \(g[pos][ch]\) 中,那么 \(pos-=siz[g[pos][ch\oplus 1]]\) ,然后向 \(g[pos][ch]\) 走;
否则加上当前位的贡献 \(|=1<<i\) ,向 \(g[pos][ch\oplus 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\n",ans>>1 );
return 0;
}
加强版
Orz Apocrypha 为什么它也出现在了我们的模拟赛里
范围:\(n\leq 7.5*10^5,k\leq \frac{n*(n+1)}{2},a_i<2^{32}\) . 于是需要一个 \(\Omicron(n\log n)\) 的优秀做法。
Solution
同样做一遍异或前缀和,记为 \(b\) ,将题目转化为点对异或。有序对和无序对的处理同上。
考虑到 \(K\) 很大,显然不能枚举,那么可以枚举每一位计算贡献。大致思路就是,找出异或值第 \(K\) 大的数,然后求所有大于这个数的异或值之和。在 Trie 上找第 \(K\) 大的思路同上,但是中间要多统计一些东西:
同原题一样,首先我们要记录一个 \(siz\) 表示 Trie 树上节点的子树大小(注意是有效的结束节点个数),统计方法同上。
设当前节点为 \(pos\) ,当前这一位的值是 \(ch\) ,那么只有在 \(g[pos][ch\oplus 1]\) 这个节点的子树中的 \(b\) 才能在这一位上产生贡献。
如果出现 \(siz[g[pos][ch\oplus 1]]<K\) ,即上文需要减去 \(siz\) 的情况,再在 \(g[pos][ch\oplus 1]\) 处打一个 \(b[i]\) 的标记,表示子树中所有的 \(b[j]\oplus b[i]\) 都会对答案产生贡献,记录 \(ans[bit]+=siz[g[pos][ch\oplus 1]]\) ,表示二进制下这一位上 \(1\) 的个数。
否则,\(ans[bit]+=K\) 。
这样做完之后,再 DFS 一遍,处理每个标记,然后加入答案的贡献即可,或者直接顺带统计也行。但是在统计答案的时候,处理标记仍然需要拆位,复杂度 \(\Omicron(n\log ^2n)\) ,还需要优化。
考虑打标记是什么形式。能够在某个点上打上标记的数的集合一定都在某一棵子树里面,那么标记就转化成了两棵子树中所有 \(b[i]\) 的两两异或和。仍然拆位维护,统计子树中某一位上 \(1\) 的个数,在插入之前将 \(b\) 排序,这样每个子树就对应一段区间,比较好处理。
复杂度之所以是 \(n\log\) 的,是因为标记个数不超过 \(\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;
}