浅谈压位 trie 及其简单应用

· · 算法·理论

Update on 2026.1.23:修改了一些错误。

突然想写就写了。

功能

可以在 O(\log_w{V})w 为计算机位长,通常取 3264)的时间复杂度、O(\frac{V}{w}) 的空间复杂度下完成以下操作:

正常用 01trie 只能做到 O(\log_2{V}),因为它是一个 2 叉树。我们发现每个位置只用存 0/1,我们直接分 w 叉不就优完了?

实现

插入删除是好做的,查询最小最大值的话用 __builtin_ctz__builtin_clz 稍微处理一下就可以了。查询前驱后继的话,跟正常的平衡树一样:我们从叶子结点往根跑,查询一下有没有比它大或小的兄弟,然后再在这里面查询最小或最大值即可。

以下是对于 V=2^{20} 的压位 trie 代码(直接展开写的,常数会小一些):

struct trie{
    #define ctz(x) __builtin_ctz(x)
    #define clz(x) __builtin_clz(x)
    int ans;
    unsigned int a0,a1[1<<5],a2[1<<10],a3[1<<15],ak;
    inline void ins(const int x)noexcept{
        a0|=1u<<(x>>15),a1[x>>15]|=1u<<((x>>10)&31),
        a2[x>>10]|=1u<<((x>>5)&31),a3[x>>5]|=1u<<(x&31);
    }
    inline void era(const int x)noexcept{
        (a3[x>>5]&=~(1u<<(x&31)))?(1):(
            (a2[x>>10]&=~(1u<<((x>>5)&31)))?(1):(
                (a1[x>>15]&=~(1u<<((x>>10)&31)))?(1):(
                    a0&=~(1u<<(x>>15))
                )
            )
        );
    }
    inline bool have(const int x)noexcept{return (a3[x>>5]>>(x&31))&1;}
    inline int minn()noexcept{
        return a0?(ans=ctz(a0),ans=(ans<<5)|ctz(a1[ans]),ans=(ans<<5)|ctz(a2[ans]),(ans<<5)|ctz(a3[ans])):(1<<20);
    }
    inline int maxn()noexcept{
        return a0?(ans=31^clz(a0),
        ans=(ans<<5)|(31^clz(a1[ans])),
        ans=(ans<<5)|(31^clz(a2[ans])),
        (ans<<5)|(31^clz(a3[ans]))):(0);
    }
    inline int suf(const int x)noexcept{
        if((ak=(a3[x>>5]>>(x&31))>>1)){
            return(x&~31u)|((x&31)+1+ctz(ak));
        }if((ak=(a2[x>>10]>>((x>>5)&31))>>1)){
            return ans=((x>>5)&~31u)|(((x>>5)&31)+1+ctz(ak)),
            (ans<<5)|ctz(a3[ans]);
        }if((ak=(a1[x>>15]>>((x>>10)&31))>>1)){
            return ans=((x>>10)&~31u)|(((x>>10)&31)+1+ctz(ak)),
            ans=(ans<<5)|ctz(a2[ans]),(ans<<5)|ctz(a3[ans]);
        }if((ak=a0>>(x>>15)>>1)){
            return ans=((x>>15)&~31u)|(((x>>15)&31)+1+ctz(ak)),
            ans=(ans<<5)|ctz(a1[ans]),ans=(ans<<5)|ctz(a2[ans]),(ans<<5)|ctz(a3[ans]);
        }return x;
    }
    inline int pre(const int x)noexcept{
        if((ak=a3[x>>5]&((1u<<(x&31))-1))){
            return(x&~31u)|(31^clz(ak));
        }if((ak=a2[x>>10]&((1u<<((x>>5)&31))-1))){
            return ans=((x>>5)&~31u)|(31^clz(ak)),
            (ans<<5)|(31^clz(a3[ans]));
        }if((ak=a1[x>>15]&((1u<<((x>>10)&31))-1))){
            return ans=((x>>10)&~31u)|(31^clz(ak)),
            ans=(ans<<5)|(31^clz(a2[ans])),(ans<<5)|(31^clz(a3[ans]));
        }if((ak=a0&((1u<<(x>>15))-1))){
            return ans=((x>>15)&~31u)|(31^clz(ak)),
            ans=(ans<<5)|(31^clz(a1[ans])),ans=(ans<<5)|(31^clz(a2[ans])),(ans<<5)|(31^clz(a3[ans]));
        }return x;
    }
};

动态开点的话就把上面的数组改成 unordered_map 即可,空间复杂度就是 \min(n\log_wV,\frac{V}{w})

对比

可能很多人对 O(\log_{w}{V}) 没什么概念,其实就是 O(\log_{2}{V}) 优化了 \frac{1}{5}\frac{1}{6},通常可以看成一个 45 的常数。

应用

由于题目中往往可以有重复元素,所以使用压位 trie 前经常得用离散化。

以 P3378 【模板】堆 为例,可以先用基数排序让每个值互不相同然后再用压位 trie 做到 O(n\log_{w}n)

trie t;
unsigned bucket[256];
#define SORTBYTE(TYPE, FR, TO, LEN, BIT) {\
    memset(bucket, 0, sizeof(bucket));\
    for (TYPE *it = (FR) + LEN; it != (FR); it--)\
        ++bucket[(it[-1].x >> BIT) & 255];\
    for (unsigned *it = bucket; it != bucket + 255; it++)\
        it[1] += it[0];\
    for (TYPE *it = (FR) + LEN; it != (FR); it--)\
        (TO)[--bucket[(it[-1].x >> BIT) & 255]] = it[-1];\
}
constexpr int N=1e6+5;
struct node{int id,x;}q[N],st[N],st2[N];int n,top;
int main(){
    cin>>n;
    for(int i=1;i<=n;++i)cin>>q[i].id,(q[i].id==1)?(cin>>q[i].x,st2[top++]={i,q[i].x}):(node{});
    SORTBYTE(node,st2,st,top, 0);SORTBYTE(node,st,st2,top, 8);
    SORTBYTE(node,st2,st,top,16);SORTBYTE(node,st,st2,top,24);SORTBYTE(node,st2,st,top,30);
    for(int i=0;i<top;++i)q[st[i].id].x=i;
    for(int i=1;i<=n;++i){
        switch(q[i].id){
            case 1:t.ins(q[i].x);break;
            case 2:cout<<st[t.minn()].x<<'\n';break;
            case 3:t.era(t.minn());break;
        }
    }
    return 0;
}

跑得有点慢,用了 200 多毫秒。

经常很多分块题你只能想到甚至只能做到类似于 O(n\sqrt{n\log_2n}) 的东西,用上压位 trie 你就可以强行创过去或抢到最优解(例如 P8078 [WC2022] 秃子酋长 就可以拿 n\sqrt{n}\log_wn 应创,并且卡不掉)。

众所周知,正常的珂朵莉树区间赋值单点查用 set 只能在非随机数据下做到 O(n\log_2n),但是不难发现这其实每次都是在插入删除或求前驱后继,直接上压位 trie 就可以做到 O(n\log_{w}n)(使用 Veb 可以做到严格 O(n\log_2\log_2n))。

以 P13983数列分块入门8 为例,当其他人在使用 O(n\log_{2}n) 的 set 维护珂朵莉树时,你可以直接悄悄地交一发 O(n\log_{w}n) 的压位 trie 并成功得到最优解。

trie t;
constexpr int N=3e5+5;
int n,m;
int nxt[N],val[N],ans;
inline void split(int l)
{
    if(t.have(l))return;
    int pr=t.pre(l);t.ins(l);
    nxt[l]=nxt[pr],val[l]=val[pr],nxt[pr]=l;
}
int main(){
    cin>>n;t.ins(n+1);
    for(int i=1;i<=n;++i)nxt[i]=i+1,cin>>val[i],t.ins(i);
    int l,r,v,ll,su;
    for(int i=1;i<=n;++i){
        cin>>l>>r>>v;ans=0;
        split(ll=l),split(++r);
        do{
            (val[ll]==v)?(su=nxt[ll],t.era(su),ans+=su-ll,ll=su):(su=nxt[ll],t.era(su),ll=su);
        }while(ll!=r);
        nxt[l]=r,val[l]=v,t.ins(r);
        cout<<ans<<'\n';
    }
    return 0;
}

还有就是:两个 01trie 进行位运算的复杂度跟 bitset 复杂度是一样的,例题待补。