1.1268. 搜索推荐系统
给你一个产品数组 products 和一个字符串 searchWord ,products 数组中每个产品都是一个字符串。
请你设计一个推荐系统,在依次输入单词 searchWord 的每一个字母后,推荐 products 数组中前缀与 searchWord 相同的最多三个产品。如果前缀相同的可推荐产品超过三个,请按字典序返回最小的三个。
请你以二维列表的形式,返回在输入 searchWord 每个字母后相应的推荐产品的列表。
示例 1:
输入:products = [“mobile”,“mouse”,“moneypot”,“monitor”,“mousepad”]
searchWord = “mouse”
输出:[
[“mobile”,“moneypot”,“monitor”],
[“mobile”,“moneypot”,“monitor”],
[“mouse”,“mousepad”],
[“mouse”,“mousepad”],
[“mouse”,“mousepad”]
]
示例 2:
输入:products = [“havana”], searchWord = “havana”
输出:[[“havana”],[“havana”],[“havana”],[“havana”],[“havana”],[“havana”]]
示例 3:
输入:products = [“bags”,“baggage”,“banner”,“box”,“cloths”], searchWord = “bags”
输出:[[“baggage”,“bags”,“banner”],[“baggage”,“bags”,“banner”],[“baggage”,“bags”],[“bags”]]
示例 4:
输入:products = [“havana”], searchWord = “tatiana”
输出:[[],[],[],[],[],[],[]]
提示
1 <= products.length <= 1000
1 <= Σ products[i].length <= 2 * 10^4
products[i] 中所有的字符都是小写英文字母。
1 <= searchWord.length <= 1000
searchWord 中所有字符都是小写英文字母。
虽然这道题在对字典树比较熟悉之后没有什么难度,不过对于我来说字典树还是一个比较新的内容所以就先讲解一下了,也算是给自己记的笔记。
这道题可以利用字典树来找出包含给定前缀的字符串,然后按照题目要求加入答案即可。我们先根据给出的product数组往字典树中插入这些字符串,然后用searchword中[0,i](0<=i<=searchword.size()-1)这个范围为前缀去字典树中查找有这些前缀的字符串,根据题意,每个前缀要找最多三个字符串并且按照字典序升序排列,这也刚好符合字典树的构造。
代码部分首先是字典树的数据结构:
struct TrieNode
{
TrieNode* next[26];
bool isend;
TrieNode(){
isend=false;
memset(next,0,sizeof(next));
}
};
//字典树的每个结点,这里是字符串字典树并且题目规定了都是小写字母,所以next结点有26个
class Trie{
public:
TrieNode* root;
Trie(){
root=nullptr;
}
//插入操作:
void insert(const string &s ){
if(!root){
root=new TrieNode();
}//没有根结点先new出来一个
TrieNode* cur=root;
for(int i=0;i<s.size();++i){
int idx=s[i]-'a';
if(cur->next[idx]==nullptr){
cur->next[idx]=new TrieNode();
}
cur=cur->next[idx];
//根据给定的字符串去看当前字典树有没有到达当前字母的路径,如果没有就new出来,最后前往这个结点。
}
cur->isend=true;
//插入完成之后将当前路径标记为true代表有该路径的字符串
}
TrieNode* match(TrieNode* start,const string &s,int i){
if(!start){
return nullptr;
}
int idx=s[i]-'a';
return start->next[idx];
}
void dfs(TrieNode* cur,vector<string>& tmp,string & prev){
if(tmp.size()==3){
return;
}
if(!cur){
return;
}
if(cur->isend){
tmp.push_back(prev);
}
for(int i=0;i<26;++i){
prev.push_back(i+'a');
dfs(cur->next[i],tmp,prev);
prev.pop_back();
}
}//这里的match和dfs函数是之后查找要用到的,在这里先不解释
};
根据product来构建字典树:
int i;
Trie tree;
for(int i=0;i<products.size();++i){
tree.insert(products[i]);
}
现在我们的字典树构建完成了,之后要做的就是查找和匹配问题了:
vector<vector<string>> ans;
TrieNode* cur=tree.root;
string prev="";
for(int i=0;i<searchWord.size();++i){
cur=tree.match(cur,searchWord,i);
//match函数的作用是判断字典树中是否有从根结点到searchword[i]的路径,也就是有没有这个前缀
//如果有就返回字典树中searchword[i]这个结点,如果不存在就返回空结点
prev+=searchWord[i];//前缀
if(!cur){
ans.push_back({});
//虽然如果不包含了某个前缀的话也不会有之后的前缀了,但是之后对于之后的前缀还是要加入空集合的不能直接break
}else {
vector<string> tmp;
tree.dfs(cur,tmp,prev);
ans.push_back(tmp);
//有的话就去字典树中搜索至多三个包含当前前缀的字符串
}
}
现在看我们的match和dfs函数:
TrieNode* match(TrieNode* start,const string &s,int i){
if(!start){
return nullptr;
}
int idx=s[i]-'a';
return start->next[idx];
//直接返回要匹配的结点位置,这里的start是一直在移动的
//如果之前一直都匹配的话start就是之前前缀的最后一个字母的位置,如果某次没有匹配成功会在之前直接返回空.
//而返回的结点我们并不知道他是否存在,所以在主函数中我们进行了判断
}
void dfs(TrieNode* cur,vector<string>& tmp,string & prev){
//首先是递归结束的条件,一种是tmp中加入了三个字符串,另一种是搜索到了字典树中不存在的字符串
if(tmp.size()==3){
return;
}
if(!cur){
return;
}
if(cur->isend){
tmp.push_back(prev);
}//如果字典树中有这条路径就直接加入
for(int i=0;i<26;++i){
prev.push_back(i+'a');
dfs(cur->next[i],tmp,prev);
prev.pop_back();//遍历当前前缀的所有路径
}
}
完整代码:
class Solution {
struct TrieNode
{
TrieNode* next[26];
bool isend;
TrieNode(){
isend=false;
memset(next,0,sizeof(next));
}
};
class Trie{
public:
TrieNode* root;
Trie(){
root=nullptr;
}
void insert(const string &s ){
if(!root){
root=new TrieNode();
}
TrieNode* cur=root;
for(int i=0;i<s.size();++i){
int idx=s[i]-'a';
if(cur->next[idx]==nullptr){
cur->next[idx]=new TrieNode();
}
cur=cur->next[idx];
}
cur->isend=true;
}
TrieNode* match(TrieNode* start,const string &s,int i){
if(!start){
return nullptr;
}
int idx=s[i]-'a';
return start->next[idx];
}
void dfs(TrieNode* cur,vector<string>& tmp,string & prev){
if(tmp.size()==3){
return;
}
if(!cur){
return;
}
if(cur->isend){
tmp.push_back(prev);
}
for(int i=0;i<26;++i){
prev.push_back(i+'a');
dfs(cur->next[i],tmp,prev);
prev.pop_back();
}
}
};
public:
vector<vector<string>> suggestedProducts(vector<string>& products, string searchWord) {
int i;
Trie tree;
for(int i=0;i<products.size();++i){
tree.insert(products[i]);
}
vector<vector<string>> ans;
TrieNode* cur=tree.root;
string prev="";
for(int i=0;i<searchWord.size();++i){
cur=tree.match(cur,searchWord,i);
prev+=searchWord[i];
if(!cur){
ans.push_back({});
}else {
vector<string> tmp;
tree.dfs(cur,tmp,prev);
ans.push_back(tmp);
}
}
return ans;
}
};
2.211. 添加与搜索单词 - 数据结构设计
请你设计一个数据结构,支持 添加新单词 和 查找字符串是否与任何先前添加的字符串匹配 。
实现词典类 WordDictionary :
WordDictionary() 初始化词典对象
void addWord(word) 将 word 添加到数据结构中,之后可以对它进行匹配
bool search(word) 如果数据结构中存在字符串与 word 匹配,则返回 true ;否则,返回 false 。word 中可能包含一些 ‘.’ ,每个 . 都可以表示任何一个字母。
示例:
输入:
[“WordDictionary”,“addWord”,“addWord”,“addWord”,“search”,“search”,“search”,“search”]
[[],[“bad”],[“dad”],[“mad”],[“pad”],[“bad”],[“.ad”],[“b…”]]
输出:
[null,null,null,null,false,true,true,true]
提示:
1 <= word.length <= 25
addWord 中的 word 由小写英文字母组成
search 中的 word 由 ‘.’ 或小写英文字母组成
最多调用 10^4 次 addWord 和 search
这个就是单纯的字典树的查找问题了,不过由于 ‘ . ’ 的存在情况又有些特殊,对于没有 ‘. ’的字符串很简单,直接去查找即可。而当字符串在搜素到‘ . ’的时候由于他可以代表任意一个字母,这样就说明了我们可以从 a 搜素到 d ,如果在这些替代中有一个符合条件,我们直接可以返回ture。
class WordDictionary {
struct TrieNode
{
TrieNode* next[26];
bool isend;
TrieNode(){
isend=false;
memset(next,0,sizeof(next));
}
};
class Trie{
public:
TrieNode* root;
Trie(){
root=nullptr;
}
void insert(const string &s ){
if(!root){
root=new TrieNode();
}
TrieNode* cur=root;
for(int i=0;i<s.size();++i){
int idx=s[i]-'a';
if(cur->next[idx]==nullptr){
cur->next[idx]=new TrieNode();
}
cur=cur->next[idx];
}
cur->isend=true;
}
bool dfs(TrieNode* cur,const string& s,int idx){
if(!cur){
return false;
}
if(idx==s.size()){
return cur->isend;
}
if(s[idx]=='.'){
for(int i=0;i<26;++i){
if(dfs(cur->next[i],s,idx+1)){
return true;
}
}
return false;
}else {
return dfs(cur->next[s[idx]-'a'],s,idx+1);
}
}
};
public:
Trie t;
WordDictionary() {
}
void addWord(string word) {
t.insert(word);
}
bool search(string word) {
return t.dfs(t.root,word,0);
}
};
3.421. 数组中两个数的最大异或值
给你一个整数数组 nums ,返回 nums[i] XOR nums[j] 的最大运算结果,其中 0 ≤ i ≤ j < n 。
示例 1:
输入:nums = [3,10,5,25,2,8]
输出:28
示例 2:
输入:nums = [0]
输出:0
示例 3:
输入:nums = [2,4]
输出:6
示例 4:
输入:nums = [8,10,2]
输出:10
示例 5:
输入:nums = [14,70,53,83,49,91,36,80,92,51,66,70]
输出:127
提示:
1 <= nums.length <= 2 * 10^4
0 <= nums[i] <= 2^31 - 1
这道题是经典的01字典树的运用了,01字典树就是把一个数字的二进制字符串从左往右加入到字典树中,字典树的路径只有两条,0和1就代表了这个数在该位的二进制数字为0或者1。
我们现在的问题是异或跟字典树又有什么关系?我们考虑异或的性质,相同为0,相异为1,对于一个给定数字我们想要在字典树中找出一个与他异或结果最大的数字,现在去看给定数字的二进制字符串,从高位到低位如果想要异或结果最大,那么就尽量使得高位的二进制数字不同,也就是说对于字典树中存储的数字我们可以去找出一个与给定的数字高位二进制不同数量尽量多的一个数出来。
而01字典树的路径之前提到过,只有0或者1,这跟异或的性质刚好吻合。这就是我们决定从高位到低位构建一个01字典树的原因。
这里你可能又会问,题目不是要求了0<=i<=j<n吗,这样是怎么保证的?这个问题其实十分简单,我们直接把给定数组的元素一开始就全部加入到字典树中,然后从第0个开始查找到最后,由于异或具有交换律,所以对于某个 j 位置的元素,在对他进行查找的时候,在他之前的所有 i 位置的元素都已经找到了最优解了,如果他们的最优解刚好是 j 那么 j找到的最优解也会是 i 也就是他之前的元素,这种情况是不会影响结果的。而如果i 的最优解不是 j 那么j 找到的最优解也只能是 j 位置之后的元素。
下面是代码:
class Solution {
#define maxbit 30//根据数据范围定义一个最大bit位,这个可以是31没有影响
struct TrieNode{
TrieNode* next[2];//01字典树每个结点只有两个子节点
int cnt;
TrieNode(){
cnt=0;
memset(next,0,sizeof(next));
}
};
class Trie{
public:
TrieNode* root;
Trie(){
root=nullptr;
}
void insert(int val){
if(root==nullptr){
root=new TrieNode();
}
//从高位到低位进行插入
TrieNode* cur=root;
for(int i=maxbit;i>=0;--i){
int idx=((val>>i)&1);
//将二进制中第i位的数字移到第0位并判断他是否为1
if(!(cur->next[idx])){
cur->next[idx]=new TrieNode();
}
cur=cur->next[idx];
}
cur->cnt++;//这个是用来确定有多少个以当前结点为末尾的数字数目,本题中用不到
}
//查找与val最大异或和的数字,val是给定的数字,cur是字典树当前的结点,bit是当前寻找的bit位
int dfs(int val,TrieNode* cur,int bit){
if(bit==-1){
return 0;
}
int idx=((val>>bit)&1)^1;
//从高位到低位尽量找到一个与val二进制中数字不同最多的数字
if(!cur->next[idx]){
idx^=1;
}//如果没有这条路径被迫去找另一条
return (1<<bit)*idx+dfs(val,cur->next[idx],bit-1);
//递归去找下一位与当前数字二进制位上数字不同的数字
//其中(1<<bit)*idx是将找到数字的该位二进制数转换为十进制
}
};
public:
int findMaximumXOR(vector<int>& nums) {
Trie t;
int i=0;
for(i=0;i<nums.size();++i){
t.insert(nums[i]);
}//先插入所有nums数组中元素
int ans=INT_MIN;
for(i=0;i<nums.size();++i){
ans=max(ans,t.dfs(nums[i],t.root,maxbit)^nums[i]);
}//去对nums数组中每个元素查找他的最大异或和并求出他们的最大值
return ans;
}
};
4.1707. 与数组中元素的最大异或值
给你一个由非负整数组成的数组 nums 。另有一个查询数组 queries ,其中 queries[i] = [xi, mi] 。
第 i 个查询的答案是 xi 和任何 nums 数组中不超过 mi 的元素按位异或(XOR)得到的最大值。换句话说,答案是 max(nums[j] XOR xi) ,其中所有 j 均满足 nums[j] <= mi 。如果 nums 中的所有元素都大于 mi,最终答案就是 -1 。
返回一个整数数组 answer 作为查询的答案,其中 answer.length == queries.length 且 answer[i] 是第 i 个查询的答案。
示例 1:
输入:nums = [0,1,2,3,4], queries = [[3,1],[1,3],[5,6]]
输出:[3,3,7]
示例 2:
输入:nums = [5,2,4,6,6,3], queries = [[12,4],[8,1],[6,3]]
输出:[15,-1,5]
提示:
1 <= nums.length, queries.length <= 10^5
queries[i].length == 2
0 <= nums[j], xi, mi <= 10^9
读完题后,我们首先确定这道题肯定还是利用01字典树来对每次询问进行查找。可以有了mi的限制就使得本题麻烦了很多,因为在查找的时候要时刻考虑m的存在,试想一下对于每次dfs不管是我们先去去确定路径,还是确定当前路径是否比m大,在不合法的时候我们都要进行回溯,在数据量小的时候这样做当然可以,但是观察数据0<=m<=10^9 ,0<= queries.length<=10^5这样做肯定会t的。
现在我们的主要问题就是m,能不能找到一种方法在查找中不考虑m的存在呢?答案是可以的,我们把queries按照m来进行从小到达排序,并且在排序queries的每个元素后面加入一个元素作为他们在答案数组中的原始下标。同样的nums也从小到大来进行排序,这样做是为了消除m在查找过程中的影响。
现在我们把插入和搜素放在同一个过程中进行,先定义一个遍历numsidx代表当前nums数组我们到达的位置,对于每个queries的操作我们先记录下他的xi,mi,以及我们加入的原始下标ri。在进行查找之前先进行插入操作,当numsidx没有越界,也就是没有超过nums的元素数量,并且nums[numsudx]<=mi的情况下对nums[numsjdx]进行插入,直到这两个条件有一个不满足。这样由于我们对于询问是按照mi的升序排列,并且nums也是按照升序排列的。所以字典树中每次在进行查找的时候都是不大于mi的元素,这样就把mi的影响消除了,剩下的就是01字典树的查找问题了。这种先考虑输入数据再看原始数据的方法真的很妙。
class Solution {
#define maxbit 29//依然是根据题目数据自定义的最大比特位,31当然也没有影响
struct TrieNode{
TrieNode* next[2];
int cnt;
TrieNode(){
memset(next,0,sizeof(next));
cnt=0;
}
};
class Trie{
public:
TrieNode* root;
Trie(){
root=nullptr;
}
void insert(int val){
if(!root){
root=new TrieNode();
}
TrieNode* cur=root;
for(int i=maxbit;i>=0;--i){
int idx=(val>>i)&1;
if(!cur->next[idx]){
cur->next[idx]=new TrieNode();
}
cur=cur->next[idx];
}
cur->cnt++;
}
int dfs(int val,TrieNode* cur,int bit){
if(bit==-1){
return 0;
}
int idx=(val>>bit)&1^1;
if(!cur->next[idx]){
idx^=1;
}
return (1<<bit)*idx+dfs(val,cur->next[idx],bit-1);
}//从这里可以看出01字典树没有任何改变,只是我们的解题思路发生了变化
};
public:
vector<int> maximizeXor(vector<int>& nums, vector<vector<int>>& queries) {
int i;
vector<int> ans;
Trie t;
sort(nums.begin(),nums.end());//对nums进行排序
for(i=0;i<queries.size();++i){
queries[i].push_back(i);
ans.push_back(-1);
}//对queries加入他在答案数组中的原始下标,ans也象征性的加入一些元素方便索引
sort(queries.begin(),queries.end(),[&](vector<int>& a,vector<int>&b){
return a[1]<b[1];
}
);//根据mi进行升序排列
int numsidx=0;
for(i=0;i<queries.size();++i){
int xi=queries[i][0];
int mi=queries[i][1];
int ri=queries[i][2];
while(numsidx<nums.size()&&nums[numsidx]<=mi){
t.insert(nums[numsidx]);
++numsidx;
}//nums不越界并且当前元素小于mi的情况下,字典树中插入nums[numsidx]
if(numsidx){
ans[ri]=t.dfs(xi,t.root,maxbit)^xi;
}//如果numsidx不是0就代表者字典树中存在元素可以开始查找了
}
return ans;//最后返回答案数组
}
};