【题解】异或粽子&加强版

题目链接

不愧是大常数选手,无论原题还是加强都要开 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;
}
posted @ 2021-01-05 14:47  MontesquieuE  阅读(367)  评论(0编辑  收藏  举报