【每日一题Day78】LC1803统计异或值在范围内的数对有多少 | 字典树+容斥原理

简介: 不过如果在工程中,不考虑前缀匹配的话,基本上使用 hash 就能满足。如果考虑前缀匹配的话,工程也不会使用 Trie 。一方面是字符集大小不好确定,另外,对于个别的超长字符 Trie 会进一步变深。

字典树


CPU爆炸


理论基础


  • Trie 树(又叫「前缀树」或「字典树」)是一种用于快速查询「某个字符串/字符前缀」是否存在的数据结构。


  • 其核心是使用「边」来代表有无字符,使用「点」来记录是否为「单词结尾」以及「其后续字符串的字符是什么」。


  • 应用:


。在纯算法领域,前缀树算是一种较为常用的数据结构。


。不过如果在工程中,不考虑前缀匹配的话,基本上使用 hash 就能满足。如果考虑前缀匹配的话,工程也不会使用 Trie 。一方面是字符集大小不好确定,另外,对于个别的超长字符 Trie 会进一步变深。


  • 模板:添加字符串至字典树,查询时返回次数


。二维数组


  • 使用二维数组 trie[]来存储我们所有的单词字符。
  • 使用 index 来自增记录我们到底用了多少个格子(相当于给被用到格子进行编号)。
  • 使用 count[]数组记录某个格子被「被标记为结尾的次数」(当 idx 编号的格子被标记了 n 次,则有 count[idx]=n)。


class Trie {
    int N = 100009; // N为节点个数,直接设置为十万级
    int[][] trie;
    int[] count;
    int index;
    public Trie() {
        trie = new int[N][26];
        count = new int[N];
        index = 0;
    }
    public void insert(String s) {// 将字符串str添加进字典树
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (trie[p][u] == 0) trie[p][u] = ++index;// 创建结点并赋予编号index
            p = trie[p][u];// 走到下一个结点
        }
        count[p]++;// 计数
    }
    public int search(String s) {// 返回当前字符串出现的次数
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (trie[p][u] == 0) return false;//若当前结点不存在,那么直接返回0
            p = trie[p][u];
        }
        return count[p];
    }    
}


  • 复杂度


  • 时间复杂度:O(len),len为入参字符串长度
  • 空间复杂度:O(nk),n为节点数量,k为字符集大小


。动态扩点


建立 TrieNode 结构节点。


class Trie {
    class TrieNode {
        int cnt;
        TrieNode[] tns = new TrieNode[26];
    }
    TrieNode root;
    public Trie() {
        root = new TrieNode();
    }
    public void insert(String s) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.tns[u] == null) p.tns[u] = new TrieNode();//创建结点
            p = p.tns[u]; 
        }
        p.cnt++;
    }
    public int search(String s) {
        TrieNode p = root;
        for(int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (p.tns[u] == null) return 0;
            p = p.tns[u]; 
        }
        return p.cnt;
    }
}


  • 复杂度


  • 时间复杂度:O(len),len为入参字符串长度
  • 空间复杂度:O(nk),n 为节点数量,k为字符集大小


。静态数组


减小空间复杂度,并避免垃圾回收


class Trie {
    // 以下 static 成员独一份,被创建的多个 Trie 共用
    static int N = 100009; // 直接设置为十万级
    static int[][] trie = new int[N][26];
    static int[] count = new int[N];
    static int index = 0;
    // 在构造方法中完成重置 static 成员数组的操作
    // 这样做的目的是为减少 new 操作(无论有多少测试数据,上述 static 成员只会被 new 一次)
    public Trie() {
        for (int row = index; row >= 0; row--) {
            Arrays.fill(trie[row], 0);
        }
        Arrays.fill(count, 0);
        index = 0;
    }
    public void insert(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (trie[p][u] == 0) trie[p][u] = ++index;
            p = trie[p][u];
        }
        count[p]++;
    }
    public int search(String s) {
        int p = 0;
        for (int i = 0; i < s.length(); i++) {
            int u = s.charAt(i) - 'a';
            if (trie[p][u] == 0) return 0;
            p = trie[p][u];
        }
        return count[p];
    }
}


  • 复杂度


  • 时间复杂度:O(len),len为入参字符串长度
  • 空间复杂度:O(nk),n为节点数量,k 为字符集大小


相关题目


统计异或值在范围内的数对有多少【LC1803】


Given a (0-indexed) integer array nums and two integers low and high, return the number of nice pairs.


A nice pair is a pair (i, j) where 0 <= i < j < nums.length and low <= (nums[i] XOR nums[j]) <= high.


字典树


  • 思路:


。首先使用字典树存储数组中元素的二进制形式,由于nums[i]≤2∗10 4 ,因此用15位二进制就可以表示;


。由容斥原理可得,异或值在[low,high]之间的对数=异或值为high的对数-异或值为low−1的对数


。然后求出nums[i]异或nums[0,i−1]小于等于target的数量,再将nums[i]加入字典树中


具体步骤:从高位开始枚举,符合则计数,不符合直接return


  • 如果target的第j 位为1,那么与之前数字第j 位异或结果可以为1也可以为0
  • 如果target的第j位为1,那么与之前数字第j 位异或结果只能为0


  • 二维数组实现


class Solution {
    int[][] trie;
    int[] cnt;
    int idx;
    public int countPairs(int[] nums, int low, int high) {
        trie = new int[nums.length * 16][2];
        cnt = new int[nums.length * 16];
        return get(nums, high) - get(nums, low - 1);
    }
    int get(int[] nums, int high) {
        idx = 0;
        for (int i = 0; i < trie.length; i++) trie[i][0] = trie[i][1] = cnt[i] = 0;
        int ans = 0;
        for (int i = 0; i < nums.length; i++) {
            ans += query(nums[i], high);
            add(nums[i]); 
        }
        return ans;
    }
    void add(int x) {
        int p = 0;
        for (int i = 14; i >= 0; i--) {
            int u = (x >> i)  & 1;
            if (trie[p][u] == 0) trie[p][u] = ++idx;
            p = trie[p][u]; //移动到下一个结点 
            cnt[p]++; // 个数增加,cnt[x]代表x结点出现的次数
        }
    }
    int query(int x, int high) {
        int sum = 0, p = 0;
        for  (int i = 14; i >= 0; i--) {
            int u = (x >> i) & 1;
            if (((high >> i) & 1) == 1) { //high当前i位为1, 那么x与以前数当前i位的异或可以位1或者0
                sum += cnt[trie[p][u]];//加上与x异或后当前i位为0的数量
                if (trie[p][u ^ 1] == 0) return sum; //没有结点可以继续走下去,直接返回
                p = trie[p][u ^ 1]; //继续往异或的结点走下去
            } else { //high当前i位为0, x与以前数异或的第i为必须为0
                if (trie[p][u] == 0) return sum; //没有结点走下去
                p = trie[p][u]; //寻找与x的第i位相同的进制,异或结果为0
            }
        }
        sum += cnt[p]; //加上走到最后的结点数
        return sum;
    }
}
作者:Tizzi
链接:https://leetcode.cn/problems/count-pairs-with-xor-in-a-range/solutions/2045650/javac-zi-dian-shu-fu-zi-dian-shu-mo-ban-566um/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


。复杂度


  • 时间复杂度:O(nlogC)
  • 空间复杂度:O(nlogC),n为节点数量,C为字符集大小


  • TrieNode


class Solution {
    class TrieNode{
        TrieNode[] next = new TrieNode[2];
        int cnt;
    }    
    TrieNode root;
    public int countPairs(int[] nums, int low, int high) {
        return get(nums, high) - get(nums, low - 1);
    }
    public int get(int[] nums, int t){
        root = new TrieNode();
        int ans = 0;
        for (int i = 0; i < nums.length; i++){
            ans += search(nums[i], t);
            add(nums[i]);
        }
        return ans;
    }
    public void add(int num){
        TrieNode p = root;
        for (int i = 14; i >= 0; i--){ // 高位至低位
            int u = (num >> i) & 1;
            if (p.next[u] == null) p.next[u] = new TrieNode();
            p = p.next[u];
            p.cnt++;
        }
    }
    public int search(int num, int t){
        int count = 0;
        TrieNode p = root;
        for (int i = 14; i >= 0; i--){
            int u = (num >> i) & 1;
            if (((t >> i) & 1) == 1){// t的第i位为1
                if (p.next[u] != null) count += p.next[u].cnt;// 结果为0 之后节点为任意值均符合条件 直接加cnt
                if (p.next[u ^ 1] == null) return count;// 之后没有节点可以走了 直接返回结果
                p = p.next[u ^ 1];// t的第i位为0
            }else{
                if (p.next[u] == null) return count;
                p = p.next[u];
            }
        }
        count += p.cnt;
        return count;
    }
}


。复杂度


  • 时间复杂度:O(nlogC)
  • 空间复杂度:O(nlogC),n 为节点数量,C为字符集大小


*哈希表


  • 思路:


。基于异或性质x⊕y=t等价于$y = t \oplus x $,可以统计nums中每个数的出现次数,并记录在哈希表cnt中


。然后遍历cnt的每一个键x ,那么cnt[x]∗cnt[x⊕t],累加结果除以2即为数组中任意两数异或结果为y 的对数


。那么对区间[low,high]的每个数都这样统计即可得最终结果。


  • 实现[超时]


class Solution {
    public int countPairs(int[] nums, int low, int high) {
        Map<Integer, Integer> numToCount = new HashMap<>();
        for (int num : nums){
            numToCount.put(num, numToCount.getOrDefault(num, 0) + 1);
        }
        int count = 0;
        for (int i = low; i <= high; i++){
            for (var node: numToCount.entrySet()){
                count += node.getValue() * numToCount.getOrDefault(node.getKey() ^ i,0);
            }
        }
        return count / 2;
    }
}


。复杂度


  • 时间复杂度O(n∗C),n为数组长度,C为high−low
  • 空间复杂度:O(n)


  • 优化:


。将[0,t]分成若干区间,计算每个区间的答案[没咋懂]


  • 若右数第m位(注意m>1)是1, 则可划分出一组只需考虑第m位及以左;


  • 特别地, 若右边从第1位开始连续m位是1, 则有一组, 只需考虑第m+1位及以左. (例如若 x=10011, 最后一组忽略右2位) 统一来看, 则可通过 x+1 的 第m位(m>=1)为条件进行判断, 计算异或时记得减一


13e8b92bb53365f8ca3a4cb7441031c2.png


。基于容斥原理,把[low,high]转化为计算[0,high]和[0,low−1]相减的结果


class Solution {
    public int countPairs(int[] nums, int low, int high) {
        int ans = 0;
        var cnt = new HashMap<Integer, Integer>();
        for (int x : nums) cnt.put(x, cnt.getOrDefault(x, 0) + 1);
        for (++high; high > 0; high >>= 1, low >>= 1) {
            var nxt = new HashMap<Integer, Integer>();
            for (var e : cnt.entrySet()) {
                int x = e.getKey(), c = e.getValue();
                if ((high & 1) == 1) ans += c * cnt.getOrDefault(x ^ (high - 1), 0);
                if ((low & 1) == 1)  ans -= c * cnt.getOrDefault(x ^ (low - 1), 0);
                nxt.put(x >> 1, nxt.getOrDefault(x >> 1, 0) + c);
            }
            cnt = nxt;
        }
        return ans / 2;
    }
}
作者:灵茶山艾府
链接:https://leetcode.cn/problems/count-pairs-with-xor-in-a-range/solutions/2045560/bu-hui-zi-dian-shu-zhi-yong-ha-xi-biao-y-p2pu/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


。复杂度


  • 时间复杂度:O(n),严格来说为O ( n + n l o g U /n ) ,n 为数组长度,U 为数组中的最大值
  • 空间复杂度:O ( n )
目录
相关文章
|
5月前
【每日一题Day225】LC2559统计范围内的元音字符串数 | 前缀和 二分查找
【每日一题Day225】LC2559统计范围内的元音字符串数 | 前缀和 二分查找
30 0
|
5月前
【每日一题Day370】LC318最大单词长度乘积 | 哈希表 位运算
【每日一题Day370】LC318最大单词长度乘积 | 哈希表 位运算
29 1
|
5月前
【每日一题Day161】LC1641统计字典序元音字符串的数目 | 数位dp
【每日一题Day161】LC1641统计字典序元音字符串的数目 | 数位dp
26 0
|
5月前
【每日一题Day163】LC2367算术三元组的数目 | 二分查找 哈希表
【每日一题Day163】LC2367算术三元组的数目 | 二分查找 哈希表
21 0
|
5月前
【每日一题Day205】LC2441与对应负数同时存在的最大正整数 | 哈希表
【每日一题Day205】LC2441与对应负数同时存在的最大正整数 | 哈希表
24 1
|
2月前
每日一题(最大连续1的个数,完全数计算)
每日一题(最大连续1的个数,完全数计算)
14 0
|
4月前
|
机器学习/深度学习 存储 算法
数据结构与算法面试题:给定非负整数 m 和 n,计算不大于 m 的数字中,素数的个数。(提示:算法原理为埃氏筛、线性筛)
数据结构与算法面试题:给定非负整数 m 和 n,计算不大于 m 的数字中,素数的个数。(提示:算法原理为埃氏筛、线性筛)
35 0
|
5月前
【每日一题Day368】LC421数组中两个数的最大异或值 | 字典树
【每日一题Day368】LC421数组中两个数的最大异或值 | 字典树
18 0
|
5月前
【每日一题Day358】LC2698求一个整数的惩罚数 | 递归
【每日一题Day358】LC2698求一个整数的惩罚数 | 递归
20 0
|
5月前
【每日一题Day299】LC2235两整数相加
【每日一题Day299】LC2235两整数相加
18 0

热门文章

最新文章