算法------(12)Trie树(字典树)

例题:(1)Acwing 835. Trie字符串统计

        Trie树是一个可以高效存储查询字符串的数据结构。将一个字符串的每一个字符作为一个根节点,从字符串头到字符串尾连接起来。因此我们可以把每一个字符串存储为一个节点,记录其子节点的位置。存储时对每一个字符串尾进行标记,查询时从根节点开始遍历。

#include <iostream>
#include <cstring>
#include <algorithm>
#include <string>
using namespace std;
const int N = 1e5+10,M = 26;
int son[N][M],idx,cnt[N];
void insert(string str){
    int len = str.length(),p = 0;
    for(int i = 0;i<len;i++){
        int t = str[i] - 'a';
        if(!son[p][t]) son[p][t] = ++idx;
        p = son[p][t];
    }
    cnt[p]++; 
}
int query(string str){
    int len = str.length(),p = 0;
    for(int i = 0;i<len;i++){
        int t = str[i] - 'a';
        if(!son[p][t]) return 0;
        p = son[p][t];
    }
    return cnt[p];
}
int main()
{
    int n;
    scanf("%d", &n);
    while(n--){
        char op[2];
        scanf("%s", op);
        string str;
        cin >> str;
        if(op[0] == 'I'){
            insert(str);
        }
        else{
            printf("%d\n",query(str));
        }
    }
    return 0;
}

(2) AcWing 143. 最大异或对

        暴力做法肯定会超时,因此我们对xor运算进行考虑。

        每个数可以被看做31位的二进制数,而xor运算的最大值意味着从最高位开始尽量不同。因此可以用trie树进行存储,存储每一个数并对其与前面已存在的trie树进行xor运算(查看每一位所在的节点是否有与其不同的儿子存在,如果有则*2+1,否则*2)

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 31e5+10;
int idx,son[N][2];
void insert(int n){
    int p = 0;
    for(int i = 30;i>=0;i--){
        int t = n >> i & 1;
        if(!son[p][t]) son[p][t] = ++idx;
        p = son[p][t];
    }
}
int query(int x){
    int res = 0,p = 0;
    for(int i = 30;i>=0;i--){
        int t = x >> i & 1;
        if(son[p][!t]){
            p = son[p][!t];
            res = res*2 + 1;
        }
        else{
            p = son[p][t];
            res *= 2;
        }
    }
    return res;
}
int main()
{
    int n;
    scanf("%d",&n);
    int res = 0;
    for(int i = 0;i<n;i++){
        int x;
        scanf("%d", &x);
        insert(x);
        res = max(res,query(x));
    }
    printf("%d",res);
    return 0;
}

练习:(1)Leetcode 211 添加与搜索单词

         。。生病因此状态不佳。。

        这个Trie树的查找相比一般的Trie树更为麻烦,因为他的搜索字符串中含有万能字符‘.’,所以在匹配时需要分情况讨论,一般字符正常匹配即可,而字符为‘.’时遍历判断其对应的节点是否有子节点即可。因此需要用爆搜来实现。

class WordDictionary {
public:
    int idx = 0,son[250010][26] = {0};
    bool vis[250010] = {0};
    WordDictionary() {}

    void addWord(string word) {
        int len = word.length(),p = 0;
        for(int i = 0;i<len;i++){
            int u = word[i] - 'a';
            if(!son[p][u]) son[p][u] = ++idx;
            p = son[p][u];
        }
        vis[p] = true;
    }
    
    bool search(string word) {
        return dfs(word,0,0);
    }

    bool dfs(string word,int index,int root){
        if(index == word.length()) return vis[root];
        bool f = false;
        if(word[index] == '.'){
            for(int i = 0;i<26;i++){
                if(son[root][i] == 0) continue;
                f = dfs(word,index+1,son[root][i]);
                if(f) break;
            }
        }
        else{
            if(son[root][word[index] - 'a']){
                f = dfs(word,index + 1,son[root][word[index]-'a']);
            }
        }
        return f;
    }
};

(2)Acwing 161.电话列表

         题目没什么难的。。贴上来是为了让大家注意坑点。。输入不能随意跳出循环否则剩下没输入的会顺延输入导致全部错乱。。。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 600010,M = 10;
int son[N][M],idx;
bool cnt[N];
bool query(string x){
    int len = x.length(),p = 0;
    for(int i = 0;i<len;i++){
        int u = x[i] - '0';
        if(!son[p][u]) return false;
        p = son[p][u];
        if(cnt[p]) return true;
    }
    return true;
}
void insert(string x){
    int len = x.length(),p = 0;
    for(int i = 0;i<len;i++){
        int u = x[i] - '0';
        if(!son[p][u]) son[p][u] = ++idx;
        p = son[p][u];
    }
    cnt[p] = true;
}
void init(){
    idx = 0;
    memset(son[0],0,sizeof(son));
    memset(cnt,0,sizeof(cnt));
}
int main()
{
    int t;
    scanf("%d", &t);
    while(t--){
        init();
        int n;
        bool flag = 0;
        scanf("%d", &n);
        for(int i = 0;i<n;i++){
            string str;
            cin >> str;
            if(flag) continue;
            if(!query(str)) insert(str);
            else{
                printf("NO\n");
                flag = 1;
            }
        }
        if(!flag) printf("YES\n");
    }
    return 0;
}

(3)Acwing 142. 前缀统计


         对父子节点的关系还是不太清楚。。这题其实没啥难度,主要是何时求取节点标记需要好好思考。一定是先更新到子节点,再对标记值累加。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e6+10,M = 26;
int son[N][M],idx,cnt[N];
void insert(string str){
    int p = 0,len = str.length();
    for(int i = 0;i<len;i++){
        int u = str[i] - 'a';
        if(!son[p][u]) son[p][u] = ++idx;
        p = son[p][u];
    }
    cnt[p]++;
}
int query(string str){
    int p = 0,len = str.length(),res = 0;
    for(int i = 0;i<len;i++){
        int u = str[i] - 'a';
        if(!son[p][u]) break;
        p = son[p][u];
        res += cnt[p];
    }
    return res;
}
int main()
{
    int n,m;
    scanf("%d%d", &n, &m);
    for(int i = 0;i<n;i++){
        string str;
        cin >> str;
        insert(str);
    }
    for(int i = 0;i<m;i++){
        string str;
        cin >> str;
        printf("%d\n",query(str));
    }
    return 0;
}

(4)Acwing 5304. 最高频字符串

        讲道理其实还是没啥难度的题。。还是想记录个小细节。。

        当一个函数同时存在返回值和其他附加功能的时候(比如并查集的find函数存在路径加速和返回根节点两个作用),一定要注意不要重复调用,否则可能会出现重复累加(带权并查集也是如此。。)这题的insert函数就是很好的例子。

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010,M = 26;
int son[N][M],idx,cnt[N];
int insert(string str){
    int p = 0,len = str.length();
    for(int i = 0;i<len;i++){
        int u = str[i] - 'a';
        if(!son[p][u]) son[p][u] = ++idx;
        p = son[p][u];
    }
    cnt[p]++;
    return cnt[p];
}
int main()
{
    int n;
    scanf("%d", &n);
    int res = -1;
    string ans = "";
    for(int i = 0;i<n;i++){
        string c;
        cin >> c;
        int k = insert(c);
        if(k > res || (k == res && c < ans) ){
            ans = c;
            res = k;
        }
    }
    cout << ans;
    return 0;
}

(5) Leetcode 1065.字符串的索引对

         我还以为有什么好方法。。原来也是无脑暴力。。

        把word里的字符串存入Trie树,然后枚举text中的每一个子串去Trie树中查询。

class Solution {
    int son[1010][26],idx;
    bool cnt[1010] = {0};
public:
    void insert(string x){
        int p = 0,len = x.length();
        for(int i=0;i<len;i++){
            int u = x[i] - 'a';
            if(!son[p][u]) son[p][u] = ++idx;
            p = son[p][u];
        }
        cnt[p] = true;
    }
    bool query(string x){
        int p = 0,len = x.length();
        for(int i=0;i<len;i++){
            int u = x[i] - 'a';
            if(!son[p][u]) return false;
            p = son[p][u];
        }
        return cnt[p];
    }
    vector<vector<int>> indexPairs(string text, vector<string>& words) {
        vector<vector<int>> res;
        int sz = words.size();
        for(int i = 0;i<sz;i++){
            insert(words[i]);
        }
        int len = text.length();
        for(int i = 0;i<len;i++){
            for(int j = 1;j<=len-i;j++){
                string str = text.substr(i,j);
                cout << str << endl;
                if(query(str)) res.push_back({i,i+j-1});
            }
        }
        return res;
    }
};

(6) Leetcode 14.最长公共前缀

        讲道理其实没啥难度。。想了好一会来着。。用字典树去做其实复杂了。。。不过是字典树专题。。 

        对每一个字符串先查询再存储,因为我们要找的是整个数组中字符串的公共前缀,因此我们每次查询时都需要把结果更新成最短的那个公共前缀。同时要注意特殊情况(一个字符串),第一个字符串最好先插入。

class Solution {
    int son[40010][26],idx;
public:
    void insert(string x){
        int p = 0,len = x.length();
        for(int i = 0;i<len;i++){
            int u = x[i] - 'a';
            if(!son[p][u]) son[p][u] = ++idx;
            p = son[p][u];
        }
    }
    int query(string x){
        int p = 0,len = x.length(),res = 0;
        for(int i = 0;i<len;i++){
            int u = x[i] - 'a';
            if(!son[p][u]) return res;
            p = son[p][u];
            res++;
        }
        return res;
    }
    string longestCommonPrefix(vector<string>& strs) {
        int sz = strs.size();
        int res = 100000;
        string ans = strs[0];
        insert(strs[0]);
        for(int i = 1;i<sz;i++){
            if(query(strs[i]) < res){
                res = query(strs[i]);
                ans = strs[i].substr(0,res);
                if(ans == "") return ans;
            }
            insert(strs[i]);
        }
        return ans;
    }
};

(7) Leetcode 139.单词拆分

        其实跟Trie没啥关系(因为要Trie+dfs不会。。。)用dp做的,贴上来是为了羞辱一下自己的菜。

        dp[i]指前i个字母是否能被拼凑出来,枚举前i个字母的每一个插空位j,对插空位j前面的字符串,其能否被拼凑可看dp[j],对j后面的字符,利用check函数(截取字符串)判断是否能在哈希set中找到。对于初始状态,规定dp[0] = true。

class Solution {
    bool dp[310];
    unordered_set<string> res;
public:
    bool check(string x){
        if(res.find(x) != res.end()) return true;
        return false;
    }
    bool wordBreak(string s, vector<string>& wordDict) {
        int sz = wordDict.size();
        for(int i = 0;i<sz;i++) res.insert(wordDict[i]);
        int n = s.length();
        dp[0] = true;
        for(int i = 1;i<=n;i++){
            for(int j = 0;j<i;j++){
                if(dp[j] && check(s.substr(j,i-j))){
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[n];
    }
};

 (8) Leetcode 720.词典中最长的单词

        无非就是打打标记嘛。。。and注意审题。。。

        要求其他单词“逐步”添加一个字母构成的最长单词,那么对query查找进行改装即可,只遍历数组的前n-1个字母,每一个字母处如果都有标记则符合要求。

class Solution {
    int son[30010][26],idx;
    bool cnt[30010];
public:
    void insert(string x){
        int p = 0,len = x.length();
        for(int i = 0;i<len;i++){
            int u = x[i] - 'a';
            if(!son[p][u]) son[p][u] = ++idx;
            p = son[p][u];
        }
        cnt[p] = true;
    }
    bool query(string x){
        int p = 0,len = x.length();
        if(len == 1) return true;
        for(int i = 0;i<len-1;i++){
            int u = x[i] - 'a';
            if(!son[p][u]) return false;
            p = son[p][u];
            if(!cnt[p]) return false;
        }
        return cnt[p];
    }
    string longestWord(vector<string>& words) {
        int n = words.size();
        for(int i = 0;i<n;i++){
            insert(words[i]);
        }
        string ans = "";
        int res = 0;
        for(int i = 0;i<n;i++){
            int len = words[i].length();
            string str = words[i];
            if(query(str) && (res < len|| (res == len && str < ans))){
                ans = str;
                res = len;
            }
        }
        return ans;
    }
};

(9) Leetcode 1268.搜索推荐系统

         一开始就没想出来。。且坑点巨多。。改也改了好久。。

        这道题对字典树的“节点”这个概念有很深的强化。对每一个节点,我们对应一个字符串的大根堆,每次访问这个节点时把当前字符串加入,并且如果该大根堆中字符串数目大于3时,将堆顶的字符串(字典序最大的那个)弹出。对于目标字符串,我们一个节点一个节点进行访问,如果遇到了无相应子节点的节点,则后面全部为空集,这里我们需要一个bool变量来记录是否已经出现该情况,如果只用continue判断会出错(p=0时也意味着p回到根节点)。由于每个节点对应着大根堆,因此每个节点得到的集合还需要取反。

class Solution {
    int son[200010][26],idx = 0;
    unordered_map<int,priority_queue<string>> y;
public:
    void insert(string x){
        int p = 0,len = x.length();
        for(int i = 0;i<len;i++){
            int u = x[i] - 'a';
            if(!son[p][u]) son[p][u] = ++idx;
            p = son[p][u]; 
            y[p].push(x);
            if(y[p].size() > 3) y[p].pop();
        }
    }
    vector<vector<string>> query(string x){
        vector<vector<string>> res;
        int p = 0,len = x.length();
        bool flag = false;
        for(int i = 0;i<len;i++){
            int u = x[i] - 'a';
            if(flag || !son[p][u]){
                res.push_back({});
                flag = true;
            }
            else{
                p = son[p][u]; 
                vector<string> t;
                while(y[p].size()){
                    t.push_back(y[p].top());
                    y[p].pop();
                }
                reverse(t.begin(),t.end());
                res.push_back(t);
            }
        }
        return res;
    }
    vector<vector<string>> suggestedProducts(vector<string>& products, string searchWord) {
        int n = products.size();
        for(int i = 0;i<n;i++){
            insert(products[i]);
        }
        return query(searchWord);
    }
};

(10) Leetcode 2707.字符串中的额外字符

        。。DP做不出啊啊啊啊啊。。。

        记n为字符串长度,我们对n([0,n-1])有两种划分方式,一种是把 s[n-1]当做额外字符,因此dp[n] = dp[n-1] + 1,另一种是查看[j,n-1]是否在字典中可查找到,如果可以则dp[n]又等于dp[j](相当于[j,n-1]已经处理好,只需要再处理[0,j-1])因此对每一个i枚举对应的j即可。

class Solution {
    int d[60],son[2510][26],idx;
    bool cnt[2510];
public:
    void insert(string x){
        int p = 0,len = x.length();
        for(int i = 0;i<len;i++){
            int u = x[i] - 'a';
            if(!son[p][u]) son[p][u] = ++idx;
            p = son[p][u];
        }
        cnt[p] = true;
    }
    bool query(string x){
        int p = 0,len = x.length();
        for(int i = 0;i<len;i++){
            int u = x[i] - 'a';
            if(!son[p][u]) return false;
            p = son[p][u];
        }
        return cnt[p];
    }
    int minExtraChar(string s, vector<string>& dictionary) {
        int n = dictionary.size(),len = s.length();
        for(int i = 0;i<n;i++){
            insert(dictionary[i]);
        }
        d[0] = 0;
        n = s.length();
        for(int i = 1;i<=s.length();i++){
            d[i] = d[i-1] + 1;
            for(int j = 0;j<i;j++){
                if(query(s.substr(j,i-j))) d[i] = min(d[i],d[j]);
            }
        }
        return d[n];
    }
};

         

 

 

 

 

 

 

 

 

         

  • 17
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值