题目描述
Leetcode 第233周赛的T4
解题心路历程
开端:无脑暴力解
哟呵,就这还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_limit | bit_A(bit_num) | 操作 |
---|---|---|
0 | 0 | bit_B只能为0,爬左(0 ^ 0 = 0) |
0 | 1 | bit_B只能为1,爬右(1 ^ 1 = 0) |
1 | 0 | bit_B=0,爬左必成;(0 ^ 0 = 0 < 1)再试试右 |
1 | 1 | bit_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;
}
};
希望本文能帮助到你,欢迎交流讨论!😎