压位 trie

· · 个人记录

压位 trie

参考了¥鸽鸽的论文以及有关博客

支持操作:Dynamic Predecessor,即动态维护不可重集的插入,删除,前驱/后继查询。

时间复杂度:单次操作 O(\log_w V),其中 V 为值域,w64

压位 trie 本质上是一棵 w 叉 trie,其中每一个节点都像 trie 一样对应一段值域区间。一个节点被点亮了,当且仅当其子树内存在仍在集合内的数字。

我们对于每一个节点记录一个压位的数字 a,其中第 x 位表示它的第 x 个儿子有没有被点亮(下图中根节点也应该是被点亮的,图自论文)。

分析一下空间复杂度。这个压位 trie 动态开点会磨灭其因为有二进制数位所以能够快速进行一些操作的优越性。而在不动态开点的情况下,一个有 V 个叶节点的满 w 叉树的节点数量显然是 O(V) 的。但是由于叶节点上不会记录任何信息(没有儿子),所以叶节点可以直接抛弃。所以实际上空间复杂度是 O(\frac{V}{w}) 的。

分析一下时间复杂度。显而易见的,这个树的深度是 O(\log_w V),也就是说我们可以用单次操作 O(\log_w V) 的时间解决一些问题。

如何存储一个压位 trie?由于其是一个满 w 叉树,我们有更加简便的存储方法(以更加方便地获取一个节点的位置,这点和 zkw 线段树有些相似)—— 我们对于每一层都开一个数组 A,记录同一层的那些值。具体而言,A_{i,j} 表示高度i 的,从左往右的第 j 个节点的儿子情况(即上文提到的 a)。

如何实现插入,删除和前驱后继的查询?

在代码的具体实现时,有很多技巧方法。设 w=2^g

要注意不能右移 64 !!!会炸!!!

namespace WTrie {
    #define clz(x) __builtin_clzll(x)
    #define ctz(x) __builtin_ctzll(x)
    #define e 1ull
    const int g=6,mask=63,w=64;
    int d; ull buff[1<<25],*bf=buff,*a[6];
    ull *nowa(int sz) {bf+=sz; return bf-sz;}

    void build(int V) {
        for(d=0;;d++) {
            int c=(V+(e<<(d+1)*g)-1)>>(d+1)*g; a[d]=nowa(c);
            if(c==1) return;
        }
    }
    void ins(int x) {
        rep(i,0,d) {
            int p=x>>i*g&mask, s=x>>(i+1)*g;
            if(a[i][s]&(e<<p)) return; a[i][s]|=(e<<p);
        }
    }
    void del(int x) {
        rep(i,0,d) {
            int p=x>>i*g&mask, s=x>>(i+1)*g;
            a[i][s]&=~(e<<p); if(a[i][s]) return;
        }
    }
    int pre(int x) {
        rep(i,0,d) {
            int p=x>>i*g&mask, s=x>>(i+1)*g; ull v=a[i][s], u=v&((e<<p)-1);
            if(u) {
                int res=(x>>(i+1)*g<<(i+1)*g)+((mask-clz(u))<<i*g);
                per(j,i-1,0) res+=(mask-clz(a[j][res>>(j+1)*g]))<<j*g;
                return res;
            }
        } return 0;
    }
    int suc(int x) {
        rep(i,0,d) {
            int p=x>>i*g&mask, s=x>>(i+1)*g; ull v=a[i][s], u=(v>>p)>>1; //不能用p+1
            if((v>>p)>>1) {
                int res=(x>>(i+1)*g<<(i+1)*g)+((ctz(u)+p+1)<<i*g);
                per(j,i-1,0) res+=ctz(a[j][res>>(j+1)*g])<<j*g;
                return res;
            }
        } return 0;
    }
}

测板子:https://www.luogu.com.cn/problem/U156719,不过该题样例挂了

在这题中 3\times 10^7 次,值域为 [0,2^{32}) 的随机操作,运行了约 2s(包括生成操作所用时间)。

WC2022 秃子酋长

使用莫队。记录 q_x 表示 x 的位置。每次加入一个数 x,找到前驱 y 和后继 z,对答案的贡献为 |q_z-q_x|+|q_x-q_y|-|q_z-q_y|。同理,删除一个数,找到前驱 y 和后继 z,对答案的贡献为 -|q_z-q_x|-|q_x-q_y|+|q_z-q_y|。特判没有前驱/后继的情况。插入删除前驱后继,直接上压位 trie 即可。

卡常然后搞了个鬼畜块长。

#define abs(x) ((x)>=0?(x):-(x))
int calc(int x,int y,int z,int res=0) {
    if(y) res+=abs(p[x]-p[y]);
    if(z) res+=abs(p[x]-p[z]);
    if(y&&z) res-=abs(p[y]-p[z]);
    return res;
}
void ins(int x) {
    int y=WTrie::pre(x), z=WTrie::suc(x);
    cans+=calc(x,y,z); WTrie::ins(x);
}
void del(int x) {
    int y=WTrie::pre(x), z=WTrie::suc(x);
    cans-=calc(x,y,z); WTrie::del(x);
}

signed main() {
    n=read(), m=read(), bs=n/(sqrt(m)+2)*1.2+10;
    rep(i,1,n) a[i]=read(), p[a[i]]=i, b[i]=i/bs;
    rep(i,1,m) q[i].l=read(), q[i].r=read(), q[i].id=i;
    sort(q+1,q+m+1,cmp);
    WTrie::build(1<<20);
    int l=1,r=0;
    rep(i,1,m) {
        while(l>q[i].l) ins(a[--l]);
        while(r<q[i].r) ins(a[++r]);
        while(l<q[i].l) del(a[l++]);
        while(r>q[i].r) del(a[r--]);
        ans[q[i].id]=cans;
    }
    rep(i,1,m) printf("%lld\n",ans[i]);
    return 0;
}