目录
1. 题意
给定一棵有n个节点的有根树,m组询问[idx, val],询问从根节点到的路径中与
异或的最大值
2. 方法
-
离线算法 + 01 Trie
-
思路与算法
-
01Trie
-
维护一棵Trie树,查询到当前节点时,Trie树上存放的便是从根节点到当前节点的所有元素的二进制表示,用left边表示0,用right边表示1,使用cnt标记此处的0或1是有多少个节点经过的(方便进行删除与查询),结构体定义如下
struct Trie { Trie* left; Trie* right; // cnt表示当前节点出现的次数,在插入和删除节点的时候进行维护,在查询的时候要根据cnt==0判断当前节点是否存在 int cnt; Trie() : left(nullptr), right(nullptr), cnt(0) {} };
需要注意的是本题不需要记录Trie树的结束标志,根据题意,最大val值为
<
<
, 也就是说最大的二进制位数是18(0-17),由此设计成对于每个数来说都是等高的树高即可
-
trie insert
-
依次找u的各个二进制位,如果是1,那么查看right指针是否为空,如果为空,那么新建right指针;否则,相同的步骤对应到left指针,接着把cur指针移动到left指针或者right指针上即可,需要维护cnt,表示当前Trie节点是有几个数共同占有的
-
-
trie query
-
因为要做异或运算,根据常识,相异为1,相同为0,我们只关心为1的二进制位,要使得异或值最大-->1的二进制位尽可能多-->如果val当前二进制位是1,优先找left指针,否则,优先找right指针,如果当前结果二进制位可以为1,那么ret |= (1 << i)
-
-
trie erase
-
删除的时候只需要把u所经过的节点的cnt值--即可,这意味着当前节点在字典树中被移除
-
-
-
离线算法
-
对于询问数组,我们把每个
对应的在询问数组中的下标和要询问的数val记录下来,当到达node节点时,当前字典树中存放的便是从根节点到node节点的元素,依次处理val后存入结果数组对应位置即可
-
-
dfs回溯
-
首先需要建图
-
遍历的时候,从root开始,首先执行trie_insert(u), 把当前u加入字典树,通过离线字典,我们处理与当前节点u有关的询问,询问结束后,向下遍历u的子节点,当递归返回时,意味着u及u的子节点都已询问完成,此时在trie树中消除u的影响,并向上返回
-
-
-
复杂度分析
-
时间复杂度:O((n+q)logc),n是节点的数目,q为询问的个数,对于每次添加、删除和查询操作,我们都需要logc次操作(18次,树高)
-
空间复杂度:O(n+nlogc+2*q),依次是存图,存Trie,存询问和结果数组所需要的空间
-
-
代码实现
-
struct Trie {
Trie* left;
Trie* right;
// cnt表示当前节点出现的次数,在插入和删除节点的时候进行维护,在查询的时候要根据cnt==0判断当前节点是否存在
int cnt;
Trie() : left(nullptr), right(nullptr), cnt(0) {}
};
class Solution {
private:
static constexpr int MAXD = 17;
public:
// 01 Trie + 离线
vector<int> maxGeneticDifference(vector<int>& parents, vector<vector<int>>& queries) {
// 把parent数组存为图
int n = parents.size();
vector<vector<int>> edges(n);
// edges.clear();
int root = -1;
for (int i = 0; i < n; i++) {
if (parents[i] == -1) {
root = i;
} else {
edges[parents[i]].emplace_back(i);
}
}
// dfs回溯建立字典树
// 离线
vector<vector<pair<int, int>>> stored(n);
stored.clear();
int q = queries.size();
vector<int> ans(q);
for (int i = 0; i < q; i++) {
stored[queries[i][0]].emplace_back(i, queries[i][1]);
}
Trie* r = new Trie();
// 向字典树中新增一个数
auto trie_insert = [&](int u) {
Trie* cur = r;
for (int i = MAXD; i >= 0; i--) {
if (u & (1 << i)) {
// 向右表示一个1
// 这里直接new会导致时间变得很大,正确做法是放到if语句中在需要的时候开辟空间
// Trie* right = new Trie();
if (!(cur->right)) {
cur->right = new Trie();
}
cur = cur->right;
cur->cnt++;
} else {
// 向左表示一个0
// 这里同理,在if外面new会使得时间消耗比较大
// Trie* left = new Trie();
if (!(cur->left)) {
cur->left = new Trie();
}
cur = cur->left;
cur->cnt++;
}
}
};
// 查询x在字典树中最大的异或值
auto trie_query = [&](int x) -> int {
Trie* cur = r;
int ret = 0;
for (int i = MAXD; i >= 0; i--) {
if (x & (1 << i)) {
if (cur->left && cur->left->cnt) {
ret |= (1 << i);
cur = cur->left;
} else {
cur = cur->right;
}
} else {
if (cur->right && cur->right->cnt) {
ret |= (1 << i);
cur = cur->right;
} else {
cur = cur->left;
}
}
}
return ret;
};
// 在字典树中删除一个数
auto trie_erase = [&](int u) {
Trie* cur = r;
for (int i = MAXD; i >= 0; i--) {
if (u & (1 << i)) {
cur = cur->right;
} else {
cur = cur->left;
}
cur->cnt--;
}
};
// dfs回溯, 到达当前节点时查看是否进行查询,当从当前节点结束递归时消除节点值带来的影响,保证结果正确性
function<void(int)> dfs = [&](int root) {
trie_insert(root);
// 注意这种写法,用来遍历pair数组
for (auto [idx, num] : stored[root]) {
ans[idx] = trie_query(num);
}
for (int v : edges[root]) {
dfs(v);
}
trie_erase(root);
};
dfs(root);
return ans;
}
};