01trie

01trie 是指字符集为 \(\{0,1\}\) 的 trie。01trie 可以用来维护一些数字的异或和,支持修改(删除 + 重新插入),和全局加一(即:让其所维护所有数值递增 \(1\),本质上是一种特殊的修改操作)。

如果要维护异或和,我们只需要知道某一位上 \(0\)\(1\) 个数的奇偶性即可,也就是对于数字 \(1\) 来说,当且仅当这一位上数字 \(1\) 的个数为奇数时,这一位上的数字才是 \(1\),请时刻记住这段文字:如果只是维护异或和,我们只需要知道某一位上 \(1\) 的数量即可,而不需要知道 trie 到底维护了哪些数字。

插入

像普通 trie 树一样建树即可,只不过边上存的不是字母,是第 \(i\)\(0/1\)。需要注意的是因为每个数的二进制位数可能不同,所以一般有两种解决方法:

第一种为将二进制数倒过来存,第 \(i+1\) 层表示 x>>i&1 的值,我习惯叫做低位 trie。
第二种为在二进制数前补 \(0\),从高位到低位建出 trie,我习惯叫做高位 trie。

点击查看代码
void insert(int x){//高位trie代码,低位trie只需将for循环变为由小到大即可
	int now=1;
    for(int i=K-1;i>=0;i--){
        int s=x>>i&1;
        if(!p[now].ch[s]){
            p[now].ch[s]=++cnt;
        }
        now=p[now].ch[s];
        p[now].cnt++;
    }
}

删除

因为不可能撤回之前的操作,所以只考虑减少每个点的出现次数,当一个点出现次数为 \(0\) 时,trie 树上以它为根的子树中的点出现次数肯定为 \(0\)

点击查看代码
void del(int x){
    int now=1;
    for(int i=K-1;i>=0;i--){
        int s=x>>i&1;
        now=p[now].ch[s];
        p[now].cnt--;
    }
}

询问异或最大值/最小值

这个询问既可以是给定一个值 \(x\) 求从集合中选出一个数 \(y\),求 \(x\oplus y\) 的最大值/最小值,也可以是从集合中找到 \(x,y\) 两个数,求 \(x\oplus y\) 的最大值/最小值。

求异或最大值本质上是一个贪心的过程,在向低位搜索的过程中,若能取反就取反,不能取反才取正,求最小值反过来即可,因为是向低位搜索并贪心,所以只能使用高位 trie。

点击查看代码
int query(int x){/*贴的是给定x求集合中的数与x异或的最大值
			求集合内任意两个数最大值方法一样*/
	int now=1,ans=0;
    for(int i=K-1;i>=0;i--){
        int s=x>>i&1;
        int y=p[now].ch[s^1];
        if(y&&p[y].cnt){
            ans+=((s^1)<<i);
            now=y;
        }
        else if(p[now].ch[s]){
            now=p[now].ch[s];
            ans+=(s<<i);
        }
    }
    return ans;
}

进行全局 \(+1\) 操作

考虑到二进制的加法是怎么实现的,最低位 \(+1\),然后从低位开始一连串的 \(1\)\(1\) 变成 \(0\),交换 \(u\) 的左右儿子,然后走左儿子,若没有该儿子,则说明 \(+1\)操作结束了,因为从低位开始,所以只能使用低位 trie。

点击查看代码
void update(int st,int u){
    int now=st;
    for(int i=0;i<K;i++){
        if(!now){
            return;
        }
        p[now].cnt+=p[p[now].ch[0]].cnt;
        p[now].cnt-=p[p[now].ch[1]].cnt;
        swap(p[now].ch[0],p[now].ch[1]);
        now=p[now].ch[0];
    }
}

01trie 合并

像线段树一样合并即可。

点击查看代码
int merge(int u,int v){
    if(!u||!v){
        return u|v;
    }
    p[u].cnt+=p[v].cnt;
    p[u].ch[0]=merge(p[u].ch[0],p[v].ch[0]);
    p[u].ch[1]=merge(p[u].ch[1],p[v].ch[1]);
    return u;
}

同理也可以进行可持久化合并。

点击查看代码
int merge(int u,int v){
    if(!u||!v){
        return u|v;
    }
	int x=++cnt;
    p[x].cnt+=p[v].cnt;
    p[x].ch[0]=merge(p[u].ch[0],p[v].ch[0]);
    p[x].ch[1]=merge(p[u].ch[1],p[v].ch[1]);
    return x;
}

例题

以下所有的 trie 均表示 01trie。

CF706D Vasiliy's Multiset

对 trie 直接维护插入、删除、查询最大异或值操作即可。

点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define ll long long
#define pii pair<int,int>
#define pb push_back
#define mp make_pair
using namespace std;
namespace IO{
    template<typename T>
    inline void read(T &x){
        x=0;
        int f=1;
        char ch=getchar();
        while(ch>'9'||ch<'0'){
            if(ch=='-'){
                f=-1;
    }
            ch=getchar();
        }
        while(ch>='0'&&ch<='9'){
            x=x*10+(ch-'0');
            ch=getchar();
        }
        x=(f==1?x:-x);
    }
    template<typename T>
    inline void write(T x){
        if(x<0){
            putchar('-');
            x=-x;
        }
        if(x>=10){
            write(x/10);
        }
        putchar(x%10+'0');
    }
    template<typename T>
    inline void write_endl(T x){
        write(x);
        putchar('\n');
    }
    template<typename T>
    inline void write_space(T x){
        write(x);
        putchar(' ');
    }
}
using namespace IO;
const int N=2e5+10,K=30;
int n,cnt=1;
namespace trie{
    struct node{
        int ch[2],cnt,val;
    }p[N*K];
    void insert(int x){
        int now=1;
        for(int i=K-1;i>=0;i--){
            int s=x>>i&1;
            if(!p[now].ch[s]){
                p[now].ch[s]=++cnt;
            }
            now=p[now].ch[s];
            p[now].cnt++;
        }
        p[now].val=x;
    }
    void del(int x){
        int now=1;
        for(int i=K-1;i>=0;i--){
            int s=x>>i&1;
            now=p[now].ch[s];
            p[now].cnt--;
        }
    }
    int query(int x){
        int now=1,ans=0;
        for(int i=K-1;i>=0;i--){
            int s=x>>i&1;
            int y=p[now].ch[s^1];
            if(y&&p[y].cnt){
                ans+=((s^1)<<i);
                now=y;
            }
            else if(p[now].ch[s]){
                now=p[now].ch[s];
                ans+=(s<<i);
            }
        }
        return ans;
    }
}
void solve(){
    read(n);
    trie::insert(0);
    for(int i=1;i<=n;i++){
        char opt;
        int x;
        scanf("%c",&opt);
        read(x);
        if(opt=='+'){
            trie::insert(x);
        }
        if(opt=='-'){
            trie::del(x);
        }
        if(opt=='?'){
            write_endl(trie::query(x)^x);
        }
    }
}
signed main(){
    #ifndef ONLINE_JUDGE
        freopen("1.in","r",stdin);
        freopen("1.out","w",stdout);
    #endif
    int t;
    t=1;
    while(t--){
        solve();
    }
    return 0;
}

[省选联考 2020 A 卷] 树

考虑 \(val_u\) 怎么求出来的。将组成 \(val_v(v\in son_u)\) 的数全部 \(+1\),然后进行异或,最后异或上 \(c_u\)。考虑每个点建一颗 trie,存储每个点的 \(val\) 中某一位出现了多少个 \(1\)。将 \(c_u\) 插入 \(u\) 的 trie 中,计算出 \(val_u\) 后将这棵 trie 整体 \(+1\) 后,合并到 \(fa_u\) 的 trie 树中。

点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define int long long
#define pdi pair<double,int>
#define pii pair<int,int>
#define pb push_back
#define mp make_pair
#define eps 1e-9
using namespace std;
namespace IO{
    template<typename T>
    inline void read(T &x){
        x=0;
        int f=1;
        char ch=getchar();
        while(ch>'9'||ch<'0'){
            if(ch=='-'){
                f=-1;
    }
            ch=getchar();
        }
        while(ch>='0'&&ch<='9'){
            x=x*10+(ch-'0');
            ch=getchar();
        }
        x=(f==1?x:-x);
    }
    template<typename T>
    inline void write(T x){
        if(x<0){
            putchar('-');
            x=-x;
        }
        if(x>=10){
            write(x/10);
        }
        putchar(x%10+'0');
    }
    template<typename T>
    inline void write_endl(T x){
        write(x);
        putchar('\n');
    }
    template<typename T>
    inline void write_space(T x){
        write(x);
        putchar(' ');
    }
}
using namespace IO;
const int N=6e5+10,K=22;
struct node{
    int ch[2],cnt;
}p[N*K];
vector<int>e[N];
int cnt_c[N][K],idx;
int n,rt[N],V[N],val[N];
void ins(int x,int st,int u){
    int now=st;
    for(int i=0;i<K;i++){
        int s=(x>>i)&1;
        cnt_c[u][i]+=s;
        if(!p[now].ch[s]){
            p[now].ch[s]=++idx;
        }
        now=p[now].ch[s];
        p[now].cnt++;
    }
}
int merge(int u,int v){
    if(!u||!v){
        return u|v;
    }
    p[u].cnt+=p[v].cnt;
    p[u].ch[0]=merge(p[u].ch[0],p[v].ch[0]);
    p[u].ch[1]=merge(p[u].ch[1],p[v].ch[1]);
    return u;
}
void update(int st,int u){
    int now=st;
    for(int i=0;i<K;i++){
        if(!now){
            return;
        }
        cnt_c[u][i]+=p[p[now].ch[0]].cnt;
        cnt_c[u][i]-=p[p[now].ch[1]].cnt;
        swap(p[now].ch[0],p[now].ch[1]);
        now=p[now].ch[0];
    }
}
void dfs(int u){
    rt[u]=++idx;
    ins(V[u],rt[u],u);
    for(auto v:e[u]){
        dfs(v);
        merge(rt[u],rt[v]);
        for(int j=0;j<K;j++){
            cnt_c[u][j]+=cnt_c[v][j];
        }
    }
    for(int i=0;i<K;i++){
        val[u]+=(1<<i)*(cnt_c[u][i]&1);
    }
    update(rt[u],u);
}
signed main(){
    #ifndef ONLINE_JUDGE
        freopen("1.in","r",stdin);
        freopen("1.out","w",stdout);
    #endif
    read(n);
    for(int i=1;i<=n;i++){
        read(V[i]);
    }
    for(int i=2,fa;i<=n;i++){
        read(fa);
        e[fa].pb(i);
    }
    int ans=0;
    dfs(1);
    for(int i=1;i<=n;i++){
        ans+=val[i];
    }
    write_endl(ans);
    return 0;
}

[Ynoi2010] Fusion tree

有维护异或和的操作,有 \(+1\) 的操作,

考虑到每个点建一课 trie,把和点 \(u\) 距离为 \(1\) 的点放入点 \(u\) 的 trie 中,\(+1\) 的操作可以直接在点 \(u\) 的 trie 中全局 \(+1\)

但真的可以吗?

因为这样的话每个点会被存入不止一颗 trie 中,那么全局 \(+1\) 的时候就需要再修改其它 trie ,影响时间复杂度。此时,我们就需要为每个点找到一个特殊点,使得该点的信息只存在于特殊点的 trie 中,其它与该点相连的点在询问时再特殊处理。思考与点 \(u\) 距离为 \(1\) 的点,要么就是点 \(u\) 的父亲,要么就是点 \(u\) 的儿子,很明显其中最特殊的点就是父亲了。每个点最多有一个父亲,但却可以有很多儿子。

我们把每个点的信息存入到了父亲的 trie 中,每次修改一个点只需要将这个点原来的值从父亲的 trie 中删除,再将修改后的值加入到父亲的 trie 中,这样就只剩下一个儿子的全局 \(+1\) 操作。对于这个操作,不仅需要将儿子的值全局 \(+1\),还要记录下一个 \(tot\),表示点 \(u\) 的儿子进行了多少次全局 \(+1\) 的操作,每个点在某个时刻的真实权值为该点的权值 \(-\) 该点父亲的 \(tot\) 值。

点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define ll long long
#define pdi pair<double,int>
#define pii pair<int,int>
#define pb push_back
#define mp make_pair
#define eps 1e-9
using namespace std;
namespace IO{
    template<typename T>
    inline void read(T &x){
        x=0;
        int f=1;
        char ch=getchar();
        while(ch>'9'||ch<'0'){
            if(ch=='-'){
                f=-1;
    }
            ch=getchar();
        }
        while(ch>='0'&&ch<='9'){
            x=x*10+(ch-'0');
            ch=getchar();
        }
        x=(f==1?x:-x);
    }
    template<typename T>
    inline void write(T x){
        if(x<0){
            putchar('-');
            x=-x;
        }
        if(x>=10){
            write(x/10);
        }
        putchar(x%10+'0');
    }
    template<typename T>
    inline void write_endl(T x){
        write(x);
        putchar('\n');
    }
    template<typename T>
    inline void write_space(T x){
        write(x);
        putchar(' ');
    }
}
using namespace IO;
const int N=5e5+10,K=20;
int cnt=1,cnt_c[N][K],tot[N];
int fa[N],a[N],n,q,rt[N];
vector<int>e[N];
namespace trie{
    struct node{
        int ch[2],cnt;
    }p[N*K];
    void insert(int st,int x,int u){
        int now=st;
        p[now].cnt++;
        for(int i=0;i<K;i++){
            int s=x>>i&1;
            cnt_c[u][i]+=s;
            if(!p[now].ch[s]){
                p[now].ch[s]=++cnt;
            }
            now=p[now].ch[s];
            p[now].cnt++;
        }
    }
    void del(int st,int x,int u){
        int now=st;
        p[now].cnt--;
        for(int i=0;i<K;i++){
            int s=x>>i&1;
            cnt_c[u][i]-=s;
            now=p[now].ch[s];
            p[now].cnt--;
        }
    }
    void update(int st,int u){
        int now=st;
        for(int i=0;i<K;i++){
            if(!p[now].cnt){
                return;
            }
            cnt_c[u][i]-=p[p[now].ch[1]].cnt;
            cnt_c[u][i]+=p[p[now].ch[0]].cnt;
            swap(p[now].ch[0],p[now].ch[1]);
            now=p[now].ch[0];
        }
    }
    int query(int u){
        int ans=0;
        for(int i=0;i<K;i++){
            ans=ans+(1<<i)*(cnt_c[u][i]&1);
        }
        return ans;
    }
}
int get_val(int u){
    if(fa[u]){
        return a[u]+tot[fa[u]];
    }
    return a[u];
}
void dfs(int u,int father){
    fa[u]=father;
    rt[u]=++cnt;
    for(auto v:e[u]){
        if(v==father){
            continue;
        }
        dfs(v,u);
        trie::insert(rt[u],a[v],u);
    }
}
signed main(){
    #ifndef ONLINE_JUDGE
        freopen("1.in","r",stdin);
        freopen("1.out","w",stdout);
    #endif
    read(n),read(q);
    for(int i=1;i<n;i++){
        int u,v;
        read(u),read(v);
        e[u].pb(v);
        e[v].pb(u);
    }
    for(int i=1;i<=n;i++){
        read(a[i]);
    }
    dfs(1,0);
    while(q--){
        int opt,x,y;
        read(opt);
        if(opt==1){
            read(x);
            ++tot[x];
            if(fa[x]){
                int g_fa=fa[fa[x]];
                if(g_fa){
                    trie::del(rt[g_fa],get_val(fa[x]),g_fa);
                    a[fa[x]]++;
                    trie::insert(rt[g_fa],get_val(fa[x]),g_fa);
                }
                else{
                    a[fa[x]]++;
                }
            }
            trie::update(rt[x],x);
        }
        if(opt==2){
            int x,y;
            read(x),read(y);
            if(fa[x]){
                trie::del(rt[fa[x]],get_val(x),fa[x]);
                a[x]-=y;
                trie::insert(rt[fa[x]],get_val(x),fa[x]);
            }
            else{
                a[x]-=y;
            }
        }
        if(opt==3){
            read(x);
            if(fa[x]){
                write_endl(get_val(fa[x])^trie::query(x));
            }
            else{
                write_endl(trie::query(x));
            }
        }
    }
    return 0;
}

CF888G Xor-MST

题目要求最小生成树,回顾最小生成树算法,Kruskal 和 Prim 显然不是很适合做这个题,那就只剩下了 Boruvka 了。我们借用 Boruvka 的思想,每次选出两个连通块中边权最小的边,然后合并两个联通块。

问题是怎么不重地选边权最小的边?01trie 肯定是要建的。可以发现从深度深的点的两个儿子所在子树中各选一个数出来,它们的异或和必然小于从深度浅的点的两个儿子所在子树中各选一个数出来的异或和。

所以将原树的每个节点所在子树视作一个连通块,如果一个点有两个儿子,那么需要从两个儿子所在子树中各选一个数出来,得到异或最小值,完成联通块的合并操作。

但暴力合并复杂度会炸,所以采用启发式合并,从 \(siz\) 小的子树中选一个数出来,到另一棵子树上去找最小值,最后对所有最小值取 \(\min\) 即可。

点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define ll long long
#define pdi pair<double,int>
#define pii pair<int,int>
#define pb push_back
#define mp make_pair
#define eps 1e-9
using namespace std;
namespace IO{
    template<typename T>
    inline void read(T &x){
        x=0;
        int f=1;
        char ch=getchar();
        while(ch>'9'||ch<'0'){
            if(ch=='-'){
                f=-1;
    }
            ch=getchar();
        }
        while(ch>='0'&&ch<='9'){
            x=x*10+(ch-'0');
            ch=getchar();
        }
        x=(f==1?x:-x);
    }
    template<typename T>
    inline void write(T x){
        if(x<0){
            putchar('-');
            x=-x;
        }
        if(x>=10){
            write(x/10);
        }
        putchar(x%10+'0');
    }
    template<typename T>
    inline void write_endl(T x){
        write(x);
        putchar('\n');
    }
    template<typename T>
    inline void write_space(T x){
        write(x);
        putchar(' ');
    }
}
using namespace IO;
const int N=2e5+10,M=32;
int n,a[N],tot,cnt=1;
ll ans,Pow[M];
struct node{
    int ch[2],bit;
}p[N*(M)];
vector<int>num[N*(M)];
namespace trie{
    void insert(int x){
        int now=1;
        num[now].pb(x);
        for(int i=M-1;i>=0;i--){
            int s=(x>>i)&1ll;
            if(!p[now].ch[s]){
                p[now].ch[s]=++cnt;
            }
            now=p[now].ch[s];
            x-=s*Pow[i];
            p[now].bit=i;
            num[now].pb(x);
        }
    }
    ll query(int now,int x){
        ll res=0;
        for(int i=p[now].bit-1;i>=0;i--){
            ll s=(x>>i)&1ll;
            if(p[now].ch[s]){
                now=p[now].ch[s];
            }
            else{
                now=p[now].ch[s^1ll];
                res+=Pow[p[now].bit];
            }
        }
        return res;
    }
}
ll ask(int now){
    if(num[now].size()==1){
        return 0;
    }
    if(p[now].ch[0]){;
        ans+=ask(p[now].ch[0]);
    }
    if(p[now].ch[1]){
        ans+=ask(p[now].ch[1]);
    }
    if(!p[now].ch[0]||!p[now].ch[1]){
        return 0;
    }
    ll mn=1e12,opt=1;
    if(num[p[now].ch[0]].size()>num[p[now].ch[1]].size()){
        swap(num[p[now].ch[0]],num[p[now].ch[1]]);
        opt=0;
    }
    for(auto x:num[p[now].ch[0]]){
        mn=min(mn,trie::query(p[now].ch[opt],x));
    }
    mn=mn+Pow[p[now].bit-1];
    return mn;
}
signed main(){
    #ifndef ONLINE_JUDGE
        freopen("1.in","r",stdin);
        freopen("1.out","w",stdout);
    #endif
    Pow[0]=1;
    for(int i=1;i<M;i++){
        Pow[i]=Pow[i-1]*2ll;
    }
    read(n);
    for(int i=1;i<=n;i++){
        read(a[i]);
    }
    sort(a+1,a+n+1);
    n=unique(a+1,a+n+1)-a-1;
    for(int i=1;i<=n;i++){
        trie::insert(a[i]);
    }
    ans+=ask(1);
    write_endl(ans);
    return 0;
}

[十二省联考 2019] 异或粽子

\(s_i\) 表示 \(a_{1\sim i}\) 的异或前缀和,那么一个粽子的美味度就表示为 \(s_r\oplus s_{l-1}\),题目就转化成了给定 \(n\) 个数,求两两异或的前 \(k\) 大值的和。

求异或的前 \(k\) 大值,显然是可以 01trie 维护的。

先简化一下,求一个数和一些数异或的第 \(k\) 大值。可以将后面的数丢入 01trie 中,然后像平衡树查询排名为 \(i\) 的数一样寻找即可。

那么我们得到了一个简单的思路,将 $s_{0\sim n} $放入 01trie 中,再用一个堆维护还没被取出的与每个数异或的最大值。若与 \(s_i\) 异或第 \(x\) 大的数被取了出来,将与 \(s_i\) 异或第 \(x+1\) 大的数放入堆中。因为一对数会产生 \(2\) 的贡献,最后除 \(2\) 即可,复杂度 \(O(k\log n\log V)\)

点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define int long long
#define pdi pair<double,int>
#define pii pair<int,int>
#define pb push_back
#define mp make_pair
#define eps 1e-9
using namespace std;
namespace IO{
    template<typename T>
    inline void read(T &x){
        x=0;
        int f=1;
        char ch=getchar();
        while(ch>'9'||ch<'0'){
            if(ch=='-'){
                f=-1;
    }
            ch=getchar();
        }
        while(ch>='0'&&ch<='9'){
            x=x*10+(ch-'0');
            ch=getchar();
        }
        x=(f==1?x:-x);
    }
    template<typename T>
    inline void write(T x){
        if(x<0){
            putchar('-');
            x=-x;
        }
        if(x>=10){
            write(x/10);
        }
        putchar(x%10+'0');
    }
    template<typename T>
    inline void write_endl(T x){
        write(x);
        putchar('\n');
    }
    template<typename T>
    inline void write_space(T x){
        write(x);
        putchar(' ');
    }
}
using namespace IO;
const int N=5e5+10,Lg=32;
int n,m,s[N],cnt=1;
namespace Trie{
    struct node{
        int ch[2],siz;
    }p[N*Lg];
    void ins(int x){
        int now=1;
        for(int i=Lg-1;i>=0;i--){
            int s=x>>i&1;
            if(!p[now].ch[s]){
                p[now].ch[s]=++cnt;
            }
            now=p[now].ch[s];
            p[now].siz++;
        }
    }
    int query(int x,int k){
        // cerr<<x<<' '<<k<<' ';
        int now=1,ans=0;
        for(int i=Lg-1;i>=0;i--){
            int s=x>>i&1,sum=p[p[now].ch[s^1]].siz;
            if(!p[now].ch[s^1]){
                now=p[now].ch[s];
            }
            else if(k<=sum){
                ans|=(1ll<<i);
                now=p[now].ch[s^1];
            }
            else{
                k-=sum;
                now=p[now].ch[s];
            }
        }
        // cerr<<ans<<endl;
        return ans;
    }
}
priority_queue<tuple<int,int,int>>q;
signed main(){
    #ifndef ONLINE_JUDGE
        freopen("1.in","r",stdin);
        freopen("1.out","w",stdout);
    #endif
    read(n),read(m);
    Trie::ins(0);
    for(int i=1;i<=n;i++){
        read(s[i]);
        s[i]^=s[i-1];
        Trie::ins(s[i]);
        // cerr<<s[i]<<' ';
    }
    // cerr<<endl;
    for(int i=0;i<=n;i++){
        q.push(make_tuple(Trie::query(s[i],1),i,1));
    }
    int Ans=0;
    for(int i=1;i<=m*2;i++){
        int S=get<0>(q.top()),val=get<1>(q.top()),x=get<2>(q.top());
        // cerr<<S<<' '<<val<<' '<<x<<endl;
        q.pop();
        Ans+=S;
        q.push(make_tuple(Trie::query(s[val],x+1),val,x+1));
    }
    write_endl(Ans/2);
    return 0;
}

Friends

上题的加强版,因为 \(k\) 增大到了 \(n^2\) 级别,所以所有和 \(k\) 有关系的做法都被放弃了。但是上一题告诉我们可以 \(O(\log V)\) 找到和一个数异或起来第 \(x\) 大的数,那么可以 \(O(n\log V)\) 找到异或起来大于某一个值的数对有多少。可以二分第 \(k\) 大的数是哪一个,然后在树上二分查找。

得到了第 \(k\) 大的数,那么只剩下求大于第 \(k\) 大的数之和。考虑像数位 dp 一样,只有当第 \(k\) 大数第 \(x\) 位为 \(0\) 时异或和选到 \(1\) 才有贡献。处理出 \(tr[u][i]\) 表示在 trie 上的 \(u\) 号点的子树内所包含的数中有多少数第 \(i\) 位为 \(1\)。如果某个点有贡献,直接按位加贡献即可。

总时间复杂度 \(O(n\log^2V)\)

点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define ll long long
#define pii pair<int,int>
#define pb push_back
#define mp make_pair
using namespace std;
namespace IO{
    template<typename T>
    inline void read(T &x){
        x=0;
        int f=1;
        char ch=getchar();
        while(ch>'9'||ch<'0'){
            if(ch=='-'){
                f=-1;
    }
            ch=getchar();
        }
        while(ch>='0'&&ch<='9'){
            x=x*10+(ch-'0');
            ch=getchar();
        }
        x=(f==1?x:-x);
    }
    template<typename T>
    inline void write(T x){
        if(x<0){
            putchar('-');
            x=-x;
        }
        if(x>=10){
            write(x/10);
        }
        putchar(x%10+'0');
    }
    template<typename T>
    inline void write_endl(T x){
        write(x);
        putchar('\n');
    }
    template<typename T>
    inline void write_space(T x){
        write(x);
        putchar(' ');
    }
}
using namespace IO;
const int N=5e4+10,Lg=30,mod=1e9+7,inv2=5e8+4;
int n,a[N+100],cnt=1,tot[N*(Lg+5)][Lg];
ll ans,m;
namespace trie{
    struct node{
        int ch[2];
        ll siz;
    }p[N*(Lg+5)];
    void ins(int x){
        int now=1;
        for(int i=Lg-1;i>=0;i--){
            int s=x>>i&1;
            if(!p[now].ch[s]){
                p[now].ch[s]=++cnt;
            }
            now=p[now].ch[s];
            p[now].siz++;
        }
    }
    void get(int now,int dep,ll x){
        if(!now){
            return;
        }
        if(!dep){
            for(int i=0;i<Lg;i++){
                if(x>>i&1){
                    tot[now][i]=p[now].siz;
                }
            }
            return;
        }
        get(p[now].ch[0],dep-1,x);
        get(p[now].ch[1],dep-1,x|(1<<(dep-1)));
        for(int i=0;i<Lg;i++){
            tot[now][i]+=tot[p[now].ch[0]][i]+tot[p[now].ch[1]][i];
        }
    }
    ll get_num(int x){
        ll s=0;
        for(int i=1;i<=n;i++){
            int now=1;
            for(int j=Lg-1;j>=0;j--){
                if(!now){
                    break;
                }
                int s1=a[i]>>j&1,s2=x>>j&1;
                if(!s2){
                    s+=p[p[now].ch[s1^1]].siz;
                    now=p[now].ch[s1];
                }
                else{
                    now=p[now].ch[s1^1];
                }
            }
            s+=p[now].siz;
        }
        return s/2;
    }
    void add(ll x){
        for(int i=1;i<=n;i++){
            int now=1;
            for(int j=Lg-1;j>=0;j--){
                if(!now){
                    break;
                }
                int s1=a[i]>>j&1,s2=x>>j&1,nxt=p[now].ch[s1^1];
                if(!s2&&nxt){
                    for(int k=0;k<Lg;k++){
                        int s=a[i]>>k&1;
                        if(!s){
                            ans=(ans+1ll*tot[nxt][k]*((1ll<<k)%mod))%mod;
                        }
                        else{
                            ans=(ans+1ll*((p[nxt].siz-tot[nxt][k]+mod)%mod)*((1ll<<k)%mod))%mod;
                        }
                    }
                    now=p[now].ch[s1];
                }
                else if(s2){
                    now=p[now].ch[s1^1];
                }
                else{
                    now=p[now].ch[s1];
                }
            }
            ans=(ans+1ll*p[now].siz*x%mod)%mod;
        }
        ans=1ll*ans*inv2%mod;
    }
}
void solve(){
    read(n),read(m);
    if(!m){
        write_endl(0);
        return;
    }
    for(int i=1;i<=n;i++){
        read(a[i]);
        trie::ins(a[i]);
    }
    trie::get(trie::p[1].ch[0],Lg-1,0);
    trie::get(trie::p[1].ch[1],Lg-1,1ll<<(Lg-1));
    int l=0,r=1<<30,ans_num=0;
    while(l<=r){
        int mid=(l+r)>>1;
        int tot=trie::get_num(mid);
        if(tot>=m){
            ans_num=mid;
            l=mid+1;
        }
        else{
            r=mid-1;
        }
    }
    trie::add(ans_num);
    ans=((ans-1ll*ans_num*(trie::get_num(ans_num)-m)%mod)%mod+mod)%mod;
    write_endl(ans);
}
signed main(){
    #ifndef ONLINE_JUDGE
        freopen("1.in","r",stdin);
        freopen("1.out","w",stdout);
    #endif
    int t=1;
    while(t--){
        solve();
    }
    return 0;
}

Tree and XOR

上面两道题的加强版,因为异或可以相互抵消,所以任意一条路径可以拆成两条从根到路径两端点的路径。原路径的异或和也可以变成两条路径的异或和的异或和。我们将所有点的权值定义为它到根的路径的异或和,那么问题就变成了上面两个题目所描述的问题了。

但是这题有个至关重要的问题 \(n\le 10^6,\log V\le 62\),也就意味着不止我们上面的做法用不了,甚至连 trie 树都不好建。这些问题都不管,在上个算法中,两个 \(\log\) 都是比较严格的,但是因为 trie 是一定要用的,所以只能优化掉那个二分了。在上个算法中,我们是先二分答案在判断是否为第 \(k\) 大数,其实可以直接寻找。

考虑构建完 trie 之后,在计算出前几位答案的进度下,对于每一个 \(val_i\),如果当前位与其异或得到 \(0\) 的方案数为 \(cnt\),则答案的当前位为 \(1\),否则为 \(0\)。显然对于每个 \(val_i\),都有一个与其二进制对应的链,令其在当前位的点为 \(a_i\),有一个能与其异或出答案已确定位数的链,令其在当前位的点为 \(b_i\),使用 \(siz_p\) 表示 trie 树上点 \(p\) 的子树中数的个数,可以通过 \(siz_{a_i}\)\(siz_{b_i}\) 完成对答案的计算,复杂度为 \(O(n\log V)\)。此时我们发现建一棵完整的 trie 是没有意义的,因为真正需要的点只有当前层和上一层的点,于是我们可以放弃将点一个一个插入,转而对于每一个点修建当前层,然后计算当前层对答案和每个 \(b_i\) 链的影响。因为只有两层重要,所以直接将 trie 滚动起来,这样空间复杂度也降到了 \(O(n)\)

点击查看代码
#include<bits/stdc++.h>
#define ull unsigned long long
#define ll long long
#define pii pair<int,int>
#define pdi pair<double,int>
#define pb push_back
#define eps 1e-9
#define mp make_pair
using namespace std;
namespace IO{
    template<typename T>
    inline void read(T &x){
        x=0;
        int f=1;
        char ch=getchar();
        while(ch>'9'||ch<'0'){
            if(ch=='-'){
                f=-1;
    }
            ch=getchar();
        }
        while(ch>='0'&&ch<='9'){
            x=x*10+(ch-'0');
            ch=getchar();
        }
        x=(f==1?x:-x);
    }
    template<typename T>
    inline void write(T x){
        if(x<0){
            putchar('-');
            x=-x;
        }
        if(x>=10){
            write(x/10);
        }
        putchar(x%10+'0');
    }
    template<typename T>
    inline void write_endl(T x){
        write(x);
        putchar('\n');
    }
    template<typename T>
    inline void write_space(T x){
        write(x);
        putchar(' ');
    }
}
using namespace IO;
const int N=1e6+10,Lg=62;
int n,a[N],b[N],ch[N][2],siz[N];
ll k,val[N],ans;
void solve(){
    read(n),read(k);
    a[1]=b[1]=1;
    for(int i=2,fa;i<=n;i++){
        ll x;
        read(fa),read(x);
        val[i]=val[fa]^x;
        a[i]=b[i]=1;
    }
    for(int i=Lg-1;i>=0;i--){
        int cnt=0;
        ll sum=0;
        for(int j=1;j<=n;j++){
            ch[j][0]=ch[j][1]=siz[j]=0;
        }
        for(int j=1;j<=n;j++){
            int &p=ch[a[j]][val[j]>>i&1];
            if(!p){
                p=++cnt;
            }
            a[j]=p;
            siz[a[j]]++;
        }
        for(int j=1;j<=n;j++){
            sum+=siz[ch[b[j]][val[j]>>i&1]];
        }
        int now=0;
        if(sum<k){
            k-=sum;
            now=1;
            ans|=(1ll<<i);
        }
        for(int j=1;j<=n;j++){
            b[j]=ch[b[j]][val[j]>>i&1^now];
        }
    }
    write_endl(ans);
}
signed main(){
    #ifndef ONLINE_JUDGE
        freopen("1.in","r",stdin);
        freopen("1.out","w",stdout);
    #endif
    int t=1;
    while(t--){
        solve();
    }
    return 0;
}
posted @ 2023-09-05 15:08  luo_shen  阅读(300)  评论(0编辑  收藏  举报