Leetcode第233周赛 统计异或值在范围内的数对有多少

本文介绍了LeetCode第233周赛中的一道难题——统计异或值在指定范围内的数对。作者从暴力解法开始,逐步优化至使用字典树(Trie)降低时间复杂度,最终实现了一个高效的解决方案。文章详细阐述了解题思路,包括暴力解法的超时问题、频率统计的优化以及字典树的应用,帮助读者理解如何运用数据结构解决复杂问题。
摘要由CSDN通过智能技术生成

题目描述

Leetcode 第233周赛的T4

5696. 统计异或值在范围内的数对有多少(hard) 👈

解题心路历程

开端:无脑暴力解

哟呵,就这还hard,不是直接双重循环就AC了嘛,撑死在时间上卡人

(脑中开始计算复杂度:0(n^2),20000*20000 = 4 * 10^8,四亿,嗯还好吧)

class Solution {
public:
    int countPairs(vector<int>& nums, int low, int high) {
        int len = nums.size();
        int tmp,x;
        int ans = 0;
        for(int i=0;i<len-1;i++) {
            x = nums[i];
            for(int j=i+1;j<len;j++) {
                tmp = x ^ nums[j];
                if(tmp >= low && tmp <= high) ans++;
            }
        }
        return ans;
    }
};

很好,果然直接超时了😢(测试点:48/61)

被逼无奈开始另寻它路(听说暴力解只有js能过)

另寻他路:逆向思维

说不定有很多重复的数字呀,没必要异或那么多次的

我们可以记录频率再返回去范围里找

class Solution {
    int freq[20001];
public:
    int countPairs(vector<int>& nums, int low, int high) {
        int len = nums.size();
        int ans = 0;
        int minn = INT_MAX;
        int maxn = 0;
        for(int i=0;i<len;i++) {
            freq[nums[i]]++;
            maxn = max(maxn,nums[i]);
            minn = min(minn,nums[i]);
        }
        for(int i=minn;i<=maxn;i++) {
            if(freq[i]==0)continue;
            for(int j=low;j<=high;j++) {
                int tmp = i ^ j;
                if(tmp >=1 && tmp <=20000) {
                    ans += freq[i] * freq[tmp];
                }               
            }
        }
        return ans>>1;
    }
};

这下总可以了吧

很好,它又超时了😢(测试点:60/61,差一个点,晕了)

救星:字典树Trie的妙用

脑子榨干了也想不出来了啊,就算在暴力解下,把vector换成数组,也是过不去的(56/61)

让我们静下来想一想,异或有什么性质:(假设有三个数字,满足等式:a ^ b = c)

a ^ c = b
b ^ c = a
a ^ a = 0

说人话就是:知2推1 + 自爆(好像更迷了?)

那怎么操作呢?

傻不隆冬的我不得不看了波题解以及B站UP喂你脚下有坑的视频,开始有了头绪

用的就是【字典树Trie】这一数据结构(学习参考视频:【数据结构 10】Trie|前缀树|字典树

核心就在于insert()query()

主要思想:

插入当前数字前,判断目前树中有多少与输入值异或后比high + 1小的数字,有多少比low小的数字,这两者相减,就是我们需要的值(大段 - 小段 = 获得区间)

为什么要先查再插呢?

是为了保证当前的num只和之前的数比较,不会重复(即握手原则,不会和自己握手)

过程模拟

为什么行得通,其实就是把一个int转为二进制后序列化为字符串

比如:6序列化为110,我们可以这么表示【右-右-左】(0:左爬;1:右爬;结点值:size,代表节点下的串的数量)

0
 \
  1
   \
    1
   /
  1

而左右呢,我们只要用一个Trie[2]就可以啦

如果是26个字母的话,我们可以利用ASCII码特性,Trie[26]就可以了,做个映射,比如:Trie[i-‘a’]

代入实际例子,假设输入为[1,4,2,3],此时已经插入了前三个,准备做query(3,5)(即<=4的异或数)

当前状态:

A:011

limit:101

      Root
    /      \
   2        1
 /   \     /
1     1  1
 \   /  /
 1  1  1

我们首先要知道:A ^ B = limit; A ^ limit = B

据此,我们来分析一下B(也就是分析字典树):

  • 先爬左:0xx,A、B异或,首位仍为0,必定小于limit的101,ans+=2
  • 先走右:1xx,A、B异或,首位仍为1,和limit一样,这就不一定了,得就这往下爬
  • 这时只剩一条路,10x,A、B异或为11x,大于101了,不考虑
  • 最终ans = 2
  • 以此类推…

列表总结

我们来列个表总结一下几种情况:(用A ^ B = limit)

bit_limitbit_A(bit_num)操作
00bit_B只能为0,爬左(0 ^ 0 = 0)
01bit_B只能为1,爬右(1 ^ 1 = 0)
10bit_B=0,爬左必成;(0 ^ 0 = 0 < 1)再试试右
11bit_B=1,爬右必成;(1 ^ 1 = 0 < 1)再试试左

优化

这样一来,我们的时间复杂度,就从O(n^2)降到了O(nk)

k的范围也就是我们的字符串长,在本题中,数的范围是<=20000的,也就是不超过16位(65536),k可远小于20000呀!

最终代码

struct Trie {
    int size;
    Trie* ch[2];
};

class Solution {
    Trie* root;
    void init(Trie *t) {
        t->size = 0;
        t->ch[0] = t->ch[1] = nullptr;
    }
public:
    void insert(int num) {
        Trie *node = root;
        int bit;
        // 为什么选15->0,16位
        // 2^16 = 65536 > 20000(题目范围) 
        for(int i=15;i>=0;i--) {
            bit = (num >> i) & 1;
            if(node->ch[bit] == nullptr) {
                node->ch[bit] = new Trie;
                init(node->ch[bit]);
            }
            node = node->ch[bit];
            node->size++;
        }
    }

    int query(int num, int limit) {
        Trie *node = root;
        int res = 0;
        int bit_num, bit_lim;
        for(int i=15;i>=0;i--) {
            if(node == nullptr) return res;
            bit_num = (num>>i) & 1;
            bit_lim = (limit>>i) & 1;
            if(bit_lim) {
                if(node->ch[bit_num] != nullptr) {
                    res += node->ch[bit_num]->size;
                }
                node = node->ch[1-bit_num];
            } else {
                node = node->ch[bit_num];
            }
        }
        return res;
    }

    int countPairs(vector<int>& nums, int low, int high) {
        int ans = 0;
        root = new Trie; init(root);
        for(int num : nums) {
            ans += query(num, high+1) - query(num, low);
            insert(num);
        }
        return ans;
    }
};

希望本文能帮助到你,欢迎交流讨论!😎

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值