01字典树和可持久化01字典树

01字典树

01字典树是一种只有0和1两种边的字典树。可以解决查询第 \(k\) 小,查询 \(x\) 是第几小等问题。

查询第 \(k\)

可以把输入的数转成等长二进制,然后插入01字典树。比如将 \([0,0,1,3,3]\) 插入字典树:

这里红色数字表示以该段为前缀的数的个数,黑色表示对应的数。

假设我们要查询第 \(3\) 小,可以发现根节点的左子树里有 \(3\) 个数,即第 \(3\) 小在左子树内:

接着看,左子树内只有两个数,说明答案在右子树内:

最终我们就得到了答案 \(1\)

观察这个过程,可以发现第 \(k\) 小的求法:

  • 当左子树内的数的个数 \(cnt< k\),则答案在右子树内,\(k\leftarrow k-cnt\)
  • 否则答案在左子树内。

不断递归上述过程,直到找到答案。

空间复杂度 \(O(N \log \max \{a_i\})\),单次查询时间复杂度 \(O(\log \max \{a_i\})\)

代码

int GetNum(int k) {
    int u = 1, ans = 0;
    for(int i = 29; i >= 0; --i) {
      if(cnt[son[u][0]] < k) {
        k -= cnt[son[u][0]], u = son[u][1], ans = (ans << 1) | 1;
      }else {
        u = son[u][0], ans <<= 1;
      }
    }
    return ans;
  }

GetNum(k);

查询 \(x\) 为第几小

这个很容易明白,就不多做解释了。

重复执行以下过程:

  • 如果 \(x\) 在右子树,则 \(ans \leftarrow ans+cnt\),其中 \(ans,cnt\) 分别为答案和左子树的数的数量。
  • 否则什么也不做。
  • 走到对应的儿子上。

最终的答案就为 \(ans+1\)

空间复杂度 \(O(N \log \max \{a_i\})\),单次查询时间复杂度 \(O(\log \max \{a_i\})\)

代码

int GetRnk(int x) {
  int u = 1, ans = 1;
  for(int i = 29; i >= 0; --i) {
    if((x >> i) & 1) {
      ans += cnt[son[u][0]], u = son[u][1];
    }else {
      u = son[u][0];
    }
  }
  return ans;
}

GetRnk(x);

\(x\) 与其中一数的最大异或和

贪心地考虑即可:

  • 如果当前这位存在与 \(x\) 的不同的,则选择这一位
  • 否则不选择

空间复杂度 \(O(N \log \max \{a_i\})\),单次查询时间复杂度 \(O(\log \max \{a_i\})\)

代码

int GetMaxXor(int x) {
  int u = 1, ans = 0;
  for(int i = 29; i >= 0; --i) {
    if(cnt[son[u][!((x >> i) & 1)]) {
      u = son[u][!((x >> i) & 1)], ans = (ans << 1) | 1;
    }else {
      u = son[u][(x >> i) & 1], ans <<= 1;
    }
  }
  return ans;
}
           
GetMaxXor(x);

01字典树还有很多用处,这里就不多说了。

可持久化01字典树

可持久化01字典树和01字典树只有一个区别,它可以查询任意时刻的01字典树。

首先,我们能想到的最暴力的方法就是每次修改都复制一颗字典树。虽然这样很明显空间会爆掉,但我们还是先看看。比如在 \([0,0,1,3,3]\) 中再加入一个 \(2\)

你可能会注意到,其中大部分的点都没有变化,只有处于 \(2\) 的这条链上有变化,我们不妨这么做:

可以发现现在总共只加入了 \(O(\log \max \{a_i\})\) 个点!

而可持久化字典树也正是这样运作的,就是把不用修改的子树直接指向之前的。在查找历史版本时直接从对应的根节点搜下去就行了。

空间复杂度 \(O((N+M) \log \max \{a_i\})\),单次插入时间复杂度 \(O(\log \max \{a_i\})\)

代码

void Insert(int x, int id, int pos, int val) {
  ROOT[id] = ++tot;
  int u = tot, v = ROOT[x];
  for(int i = 19; i >= 0; --i) {
    son[u][(pos >> i) & 1] = ++tot;
    son[u][!((pos >> i) & 1)] = son[v][!((pos >> i) & 1)];
    u = son[u][(pos >> i) & 1], v = son[v][(pos >> i) & 1];
  }
  cnt[u] = val;
}

Insert(x, y, pos, val);

查询区间第 \(k\)

01字典树只能求全体第 \(k\) 小,但无法求区间第 \(k\) 小。而区间第 \(k\) 小也很简单,即两个前缀字典树相减再查询。

空间复杂度 \(O((N+M) \log \max \{a_i\})\),单次查询时间复杂度 \(O(\log \max \{a_i\})\)

代码

int GetNum(int l, int r, int k) {
  int u = ROOT[r], v = ROOT[l - 1], ans = 0;
  for(int i = 29; i >= 0; --i) {
   	if(cnt[son[u][0]] - cnt[son[v][0]] < k) {
     k -= cnt[son[u][0]] - cnt[son[v][0]], u = son[u][1], v = son[v][1], ans = (ans << 1) | 1;
    }else {
     u = son[u][0], v = son[v][0], ans <<= 1;
    }
  }
  return ans;
}

GetNum(l, r, k);

查询 \(x\) 是区间第几小

两个前缀字典树相减再查询即可。

空间复杂度 \(O((N+M) \log \max \{a_i\})\),单次查询时间复杂度 \(O(\log \max \{a_i\})\)

代码

int GetRnk(int l, int r, int x) {
  int u = ROOT[r], v = ROOT[l - 1], ans = 1;
  for(int i = 29; i >= 0; --i) {
    if((x >> i) & 1) {
      ans += cnt[son[u][0]] - cnt[son[v][0]], u = son[u][1], v = son[v][1];
    }else {
      u = son[u][0], v = son[v][0];
    }
  }
  return ans;
}

GetRnk(l, r, x);

查询区间不同数字数量

首先,我们记数字 \(a_i\) 上一次出现的位置为 \(l_i\)

很显然,当 $l_i < $ 查询区间的左端点,那么这个数字是这个区间中第一次出现。

即不同数字数量 \(= l_i<\) 区间的左端点的 \(i\) 的数量,而这个可以很轻易地用可持久化 \(01\) 字典树。

代码

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 400001;

struct Persistent_01Trie {
  int tot = 0, ROOT[MAXN], son[32 * MAXN][2], cnt[32 * MAXN];
  void Insert(int x, int y, int val) {
    ROOT[x] = ++tot;
    int u = ROOT[x], v = ROOT[y];
    for(int i = 29; i >= 0; --i) {
      son[u][(val >> i) & 1] = ++tot;
      son[u][!((val >> i) & 1)] = son[v][!((val >> i) & 1)];
      cnt[u] = cnt[v] + 1, u = son[u][(val >> i) & 1], v = son[v][(val >> i) & 1];
    }
    cnt[u] = cnt[v] + 1;
  }
  int Get(int l, int r, int k) {
    int u = ROOT[r], v = ROOT[l - 1], ans = 0;
    for(int i = 29; i >= 0; --i) {
      if((k >> i) & 1) {
        ans += cnt[son[u][0]] - cnt[son[v][0]], u = son[u][1], v = son[v][1];
      }else {
        u = son[u][0], v = son[v][0];
      }
    }
    return ans;
  }
}tr;

int n, q;
map<int, int> last;

int main() {
  ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
  cin >> n >> q;
  for(int i = 1, x; i <= n; ++i) {
    cin >> x;
    tr.Insert(i, i - 1, last[x]), last[x] = i;
  }
  for(int i = 1, l, r; i <= q; ++i) {
    cin >> l >> r;
    cout << tr.Get(l, r, l) << "\n";
  }
  return 0;
}

posted @ 2024-07-02 16:18  Yaosicheng124  阅读(34)  评论(0)    收藏  举报