字典树

思想:相同的前缀只存储一次。
如pdf所示:
文稿 2021年7月6日.pdf

用途:

  • 字符串统计
  • 求数组中的元素最大异或和

例题

835. Trie字符串统计

维护一个字符串集合,支持两种操作:

  1. I x 向集合中插入一个字符串 x;
  2. Q x 询问一个字符串在集合中出现了多少次。

共有 N 个操作,输入的字符串总长度不超过 105,字符串仅包含小写英文字母。
输入格式
第一行包含整数 N,表示操作数。
接下来 N 行,每行包含一个操作指令,指令为 I xQ x 中的一种。
输出格式
对于每个询问指令 Q x,都要输出一个整数作为结果,表示 xx 在集合中出现的次数。
每个结果占一行。
数据范围
1≤N≤2∗104
输入样例:

  1. 5
  2. I abc
  3. Q abc
  4. Q ab
  5. I ab
  6. Q ab

输出样例:

  1. 1
  2. 0
  3. 1

思路:用Trie树存储字符串,方便后续查询

  1. import java.util.*;
  2. import java.io.*;
  3. public class Main {
  4. public static void main(String[] sss) throws IOException{
  5. BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
  6. BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
  7. int m = Integer.parseInt(br.readLine());
  8. //创建根节点
  9. Trie root = new Trie();
  10. while (m-- > 0) {
  11. String[] str = br.readLine().split(" ");
  12. if (str[0].equals("I")) {
  13. root.add(str[1]);
  14. } else {
  15. bw.write(root.query(str[1]) + "\n");
  16. }
  17. }
  18. br.close();
  19. bw.close();
  20. }
  21. }
  22. class Trie {
  23. Trie[] son = new Trie[26];
  24. //count:以当前节点作为最终字符的字符串个数
  25. int count;
  26. void add(String str) {
  27. int n = str.length();
  28. Trie cur = this;
  29. for (int i = 0; i < n; i++) {
  30. int idx = str.charAt(i) - 'a';
  31. if (cur.son[idx] == null) cur.son[idx] = new Trie();
  32. cur = cur.son[idx];
  33. }
  34. cur.count++;
  35. }
  36. int query(String str) {
  37. int n = str.length();
  38. Trie cur = this;
  39. for (int i = 0; i < n; i++) {
  40. int idx = str.charAt(i) - 'a';
  41. if (cur.son[idx] == null) return 0;
  42. cur = cur.son[idx];
  43. }
  44. return cur.count;
  45. }
  46. }

143. 最大异或对

在给定的 N 个整数 A1,A2……AN 中选出两个进行 xor(异或)运算,得到的结果最大是多少?
输入格式
第一行输入一个整数 N。
第二行输入 N个整数 A1~AN。
输出格式
输出一个整数表示答案。
数据范围
1≤N≤105,
0≤Ai<231
输入样例:

  1. 3
  2. 1 2 3

输出样例:

  1. 3

思路:
用字典树存储每个数的二进制形式,注意从高位向低位进行,方便后续异或查找
对于数组中的每个数,查询字典树,找到最大异或值。

  1. import java.util.*;
  2. import java.io.*;
  3. public class Main {
  4. public static void main(String[] sss) throws IOException{
  5. BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
  6. BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
  7. int n = Integer.parseInt(br.readLine());
  8. int[] nums = new int[n];
  9. Trie root = new Trie();
  10. String[] str = br.readLine().split(" ");
  11. for (int i = 0; i < n; i++) nums[i] = Integer.parseInt(str[i]);
  12. //构造字典树
  13. for (int num : nums) {
  14. root.add(num);
  15. }
  16. //查询最大值
  17. int res = 0;
  18. for (int num : nums) {
  19. res = Math.max(res, root.union(num));
  20. }
  21. bw.write(Integer.toString(res));
  22. br.close();
  23. bw.close();
  24. }
  25. }
  26. class Trie {
  27. Trie[] son = new Trie[2];
  28. static final int HIGH_BIT = 30;
  29. void add(int num) {
  30. Trie cur = this;
  31. for (int i = HIGH_BIT; i >= 0; i--) {
  32. int idx = (num >> i) & 1;
  33. if (cur.son[idx] == null) cur.son[idx] = new Trie();
  34. cur = cur.son[idx];
  35. }
  36. }
  37. int union(int num) {
  38. Trie cur = this;
  39. int res = 0;
  40. for (int i = HIGH_BIT; i >= 0; i--) {
  41. int idx = (num >> i) & 1;
  42. if (cur.son[1-idx] != null) {
  43. res += (1 << i);
  44. cur = cur.son[1-idx];
  45. }
  46. else {
  47. cur = cur.son[idx];
  48. }
  49. }
  50. return res;
  51. }
  52. }

1938. 查询最大基因差

image.png
给你一棵 n 个节点的有根树,节点编号从 0 到 n - 1 。每个节点的编号表示这个节点的 独一无二的基因值 (也就是说节点 x 的基因值为 x)。两个基因值的 基因差 是两者的 异或和 。给你整数数组 parents ,其中 parents[i] 是节点 i 的父节点。如果节点 x 是树的 ,那么 parents[x] == -1 。
给你查询数组 queries ,其中 queries[i] = [nodei, vali] 。对于查询 i ,请你找到 vali 和 pi 的 最大基因差 ,其中 pi 是节点 nodei 到根之间的任意节点(包含 nodei 和根节点)。更正式的,你想要最大化 vali XOR pi 。
请你返回数组_ _ans ,其中 ans[i] 是第 i 个查询的答案。

示例 1:
输入:parents = [-1,0,1,1], queries = [[0,2],[3,2],[2,5]] 输出:[2,3,7] 解释:查询数组处理如下: - [0,2]:最大基因差的对应节点为 0 ,基因差为 2 XOR 0 = 2 。 - [3,2]:最大基因差的对应节点为 1 ,基因差为 2 XOR 1 = 3 。 - [2,5]:最大基因差的对应节点为 2 ,基因差为 5 XOR 2 = 7 。
image.png
示例 2:
输入:parents = [3,7,-1,2,0,7,0,2], queries = [[4,6],[1,15],[0,5]] 输出:[6,14,7] 解释:查询数组处理如下: - [4,6]:最大基因差的对应节点为 0 ,基因差为 6 XOR 0 = 6 。 - [1,15]:最大基因差的对应节点为 1 ,基因差为 15 XOR 1 = 14 。 - [0,5]:最大基因差的对应节点为 2 ,基因差为 5 XOR 2 = 7 。

提示:

  • 2 <= parents.length <= 105
  • 对于每个 不是 根节点的 i ,有 0 <= parents[i] <= parents.length - 1 。
  • parents[root] == -1
  • 1 <= queries.length <= 3 * 104
  • 0 <= nodei <= parents.length - 1
  • 0 <= vali <= 2 * 105

思路:
离线操作 + Trie
对树进行后序遍历,每次先在字典树中添加该节点,处理完所有与该节点相同的查询后将该节点从字典树中删除。这样每次只会处理从根节点到当前节点这一个分支。
字典树也可以进行删除操作,学到了!

  1. class Solution {
  2. Map<Integer, List<int[]>> map = new HashMap<>();
  3. int N = 100010;
  4. int[] h = new int[N], e = new int[N], ne = new int[N];
  5. int n, idx, m;
  6. int[] res;
  7. Trie trie = new Trie();
  8. public int[] maxGeneticDifference(int[] parents, int[][] queries) {
  9. n = parents.length; m = queries.length;
  10. res = new int[m];
  11. for (int i = 0; i < queries.length; i++) {
  12. int node = queries[i][0];
  13. map.computeIfAbsent(node, key->new ArrayList<>()).add(new int[]{queries[i][0], queries[i][1], i});
  14. }
  15. // System.out.println(map);
  16. Arrays.fill(h, -1);
  17. int root = 0;
  18. for (int i = 0; i < n; i++) {
  19. int a = parents[i], b = i;
  20. if (a != -1)
  21. add(a, b);
  22. else
  23. root = b;
  24. }
  25. dfs(root);
  26. return res;
  27. }
  28. void dfs(int u) {
  29. trie.insert(u);
  30. for (int i = h[u]; i != -1; i = ne[i]) {
  31. int j = e[i];
  32. dfs(j);
  33. }
  34. for (int[] p : map.getOrDefault(u, new ArrayList<>())) {
  35. int idx = p[2], v = p[1];
  36. res[idx] = trie.query(v);
  37. }
  38. trie.delete(u);
  39. }
  40. void add(int a, int b) {
  41. e[idx] = b;
  42. ne[idx] = h[a];
  43. h[a] = idx++;
  44. }
  45. }
  46. class Trie {
  47. int OFFSET = 18;
  48. class TrieNode {
  49. TrieNode[] son = new TrieNode[2];
  50. int cnt;
  51. }
  52. TrieNode root = new TrieNode();
  53. void insert(int x) {
  54. TrieNode cur = root;
  55. for (int i = OFFSET; i >= 0; i--) {
  56. int v = x >> i & 1;
  57. if (cur.son[v] == null) {
  58. cur.son[v] = new TrieNode();
  59. }
  60. cur = cur.son[v];
  61. cur.cnt++;
  62. }
  63. }
  64. int query(int x) {
  65. TrieNode cur = root;
  66. int res = 0;
  67. for (int i = OFFSET; i >= 0; i--) {
  68. int v = x >> i & 1;
  69. if (cur.son[v^1] == null) {
  70. cur = cur.son[v];
  71. } else {
  72. res |= 1 << i;
  73. cur = cur.son[v^1];
  74. }
  75. }
  76. return res;
  77. }
  78. void delete(int x) {
  79. delete(OFFSET, x, root);
  80. }
  81. void delete(int u, int x, TrieNode cur) {
  82. if (u < 0) return;
  83. TrieNode ne = cur.son[x >> u & 1];
  84. delete(u - 1, x, ne);
  85. if (ne.cnt > 1) ne.cnt--;
  86. else cur.son[x >> u & 1] = null;
  87. }
  88. // 找Bug用的
  89. List<String> res = new ArrayList<>();
  90. public String toString() {
  91. TrieNode cur = root;
  92. StringBuilder sb = new StringBuilder();
  93. res.clear();
  94. get(0, cur, sb);
  95. return res.toString();
  96. }
  97. void get(int cnt, TrieNode cur, StringBuilder sb) {
  98. if (cnt > OFFSET) {
  99. res.add(sb.toString());
  100. return;
  101. }
  102. for (int i = 0; i < 2; i++) {
  103. if (cur.son[i] != null) {
  104. sb.append(i);
  105. get(cnt + 1, cur.son[i], sb);
  106. sb.deleteCharAt(sb.length() - 1);
  107. }
  108. }
  109. }
  110. }