LeetCode 剑指 Offer II 前缀树(下) 专题总结

  • 📚 博客主页:⭐️这是一只小逸白的博客鸭~⭐️
  • 👉 欢迎 关注❤️点赞👍收藏⭐️评论📝
  • 😜 小逸白正在备战实习,经常更新面试题LeetCode题解,欢迎志同道合的朋友互相交流~
  • 💙 若有问题请指正,记得关注哦,感谢~

往期文章 :

第一题:后缀,
第二题:前缀,用哈希更好
第三题:01 Trie树

065. 最短的单词编码

题目:

单词数组 words 的 有效编码 由任意助记字符串 s 和下标数组 indices 组成,且满足:

  • words.length == indices.length
  • 助记字符串 s 以 ‘#’ 字符结尾
  • 对于每个下标 indices[i]s 的一个从 indices[i] 开始、到下一个 ‘#’ 字符结束(但不包括 ‘#’)的 子字符串 恰好与 words[i] 相等
    给定一个单词数组 words ,返回成功对 words 进行编码的最小助记字符串 s 的长度 。

示例:

输入:words = [“time”, “me”, “bell”]
输出:10
解释:一组有效编码为 s = “time#bell#” 和 indices = [0, 2, 5] 。
words[0] = “time” ,s 开始于 indices[0] = 0 到下一个 ‘#’ 结束的子字符串,如加粗部分所示 “time#bell#”
words[1] = “me” ,s 开始于 indices[1] = 2 到下一个 ‘#’ 结束的子字符串,如加粗部分所示 “time#bell#”
words[2] = “bell” ,s 开始于 indices[2] = 5 到下一个 ‘#’ 结束的子字符串,如加粗部分所示 “time#bell#”

提示:

  • 1 <= words.length <= 2000
  • 1 <= words[i].length <= 7
  • words[i] 仅由小写字母组成

思路:

前缀树:
题意:如果单词 XY 的后缀,那么单词 X 就不需要考虑了,因为编码 Y 的时候就同时将 X 编码了。例如,如果 words 中同时有 “me” 和 “time”,我们就可以在不改变答案的情况下不考虑 “me”。如果单词 Y 不在任何别的单词 X 的后缀中出现,那么 Y 一定是编码字符串的一部分。

  • 前缀树反向存储单词,这样可以将后缀过滤掉
  • 查询过程中找到单词结尾时判断是不是尾节点
    • yes,就添加这个单词,并且删除这个结尾,防止一样的元素重复计算 例如:time,time,time,长度为5 --> time#
    • no,代表是后缀,直接跳过结束查询
class Trie {
public:
    vector<Trie*> next;
    bool isEnd;
    Trie():next(26, nullptr),isEnd(false){}
    void insert(string& word) {
        Trie* node = this;
        // 反向插入
        for(int i = word.length() - 1; i >= 0; i--) {
            int index = word[i] - 'a';
            if(node->next[index] == nullptr) {
                node->next[index] = new Trie();
            }
            node = node->next[index];
        }
        node->isEnd = true;
    }
    int searchLen(string& word) {
        Trie* node = this;
        for(int i = word.length() - 1; i >= 0; i--) {
            int index = word[i] - 'a';
            node = node->next[index];
            // 找到单词最后一个字符,并且该字符是单词结尾
            if(node->isEnd && i == 0) {
                // 判断这个字符是不是结尾,不是的话就是后缀,不用处理
                for(int i = 0; i < 26; i++) {
                    if(node->next[i]) {
                        return 0;
                    }
                }
                // 将这个单词删除,将相同元素 adn 后缀元素删除
                node->isEnd = false;
                // 这个字符是结尾,返回 单词 + #
                return word.length() + 1;
            }
        }
        return 0;
    }
};

class Solution {
public:
    // 思路:本题含义是如果 x 是 y 的后缀,就删除x,不用统计
    int minimumLengthEncoding(vector<string>& words) {
        Trie* node = new Trie();
        for(auto& word : words) {
            node->insert(word);
        }
        int ans = 0;
        for(auto& word : words) {
            ans += node->searchLen(word);
        }
        return ans;
    }
};

066. 单词之和

题目:

实现一个 MapSum 类,支持两个方法,insertsum
MapSum() 初始化 MapSum 对象
void insert(String key, int val) 插入 key-val 键值对,字符串表示键 key ,整数表示值 val 。如果键 key 已经存在,那么原来的键值对将被替代成新的键值对。
int sum(string prefix) 返回所有以该前缀 prefix 开头的键 key 的值的总和。

示例:

输入:
inputs = [“MapSum”, “insert”, “sum”, “insert”, “sum”]
inputs = [[], [“apple”, 3], [“ap”], [“app”, 2], [“ap”]]
输出:
[null, null, 3, null, 5]
解释:
MapSum mapSum = new MapSum();
mapSum.insert(“apple”, 3);
mapSum.sum(“ap”); // return 3 (apple = 3)
mapSum.insert(“app”, 2);
mapSum.sum(“ap”); // return 5 (apple + app = 3 + 2 = 5)

提示:

  • 1 <= key.length, prefix.length <= 50
  • keyprefix 仅由小写英文字母组成
  • 1 <= val <= 1000
  • 最多调用 50insertsum

思路:

这道题用哈希表反而比前缀树简单不止一点半点,而且还快
题意:难点是 sum()函数,要找到prefix前缀的单词的val

方法一:哈希表

将单词存入哈希表,求和时遍历哈希表,配对前缀为prefix的单词
substr()进行前缀配对
在这里插入图片描述

class MapSum {
public:
    unordered_map<string, int> map;
    MapSum() {
    }
    
    void insert(string key, int val) {
        map[key] = val;
    }
    
    int sum(string prefix) {
        int ans = 0, n = prefix.length();
        for(auto& [key, val] : map) {
            if(key.length() >= n && key.substr(0, n) == prefix)
                ans += val;
        }
        return ans;
    }
};

方法二:前缀树

正常插入,单词结尾时用wordVal存储val值,将bool标记换成int标记
计算sum()时搜索按前缀开头的单词

  • 先按prefix前缀顺序搜索
  • 接着不确定哪里有字符, 用dfs深搜,将26个可能的后序字符都搜索一遍
  • dfs过程中用ans将单词val加上

在这里插入图片描述

class Trie {
private:
    int wordVal;
    vector<Trie*> next;
public:
    Trie():next(26,nullptr),wordVal(0){};
    // 插入,将结尾的bool 换成 int 就行
    void insert(string& word, int val) {
        Trie* node = this;
        for(char ch : word) {
            int index = ch - 'a';
            if(node->next[index] == nullptr) {
                node->next[index] = new Trie();
            }
            node = node->next[index];
        }
        node->wordVal = val;
    }
    // 先顺着前缀搜索下去,后面转而遍历每一种可能
    void searchPrefix(string& word, int& ans) {
        Trie* node = this;
        // 找到前缀的最后一个字符
        for(auto& ch : word) {
            // 没有word这个前缀就直接返回
            if(node->next[ch - 'a'] == nullptr)
                return ;
            node = node->next[ch - 'a'];
        }
        // 从这个字符开始遍历
        dfs(node, ans);
    }
    void dfs(Trie* node, int& ans) {
        if(node == nullptr) return ;

        // 如果是单词就加上
        if(node->wordVal > 0) 
            ans += node->wordVal;
        // 遍历前缀剩余可能的部分
        for(int i = 0; i < 26; i++) {
            dfs(node->next[i], ans);
        }
    }
};

class MapSum {
public:
    Trie* node;
    MapSum() {
        node = new Trie();
    }
    
    void insert(string key, int val) {
        node->insert(key, val);
    }
    
    int sum(string prefix) {
        int ans = 0;
        node->searchPrefix(prefix, ans);
        return ans;
    }
};

067. 最大的异或

题目:

给定一个整数数组 nums ,返回 nums[i] XOR nums[j] 的最大运算结果,其中 0 ≤ i ≤ j < n

示例:

输入:nums = [3,10,5,25,2,8]
输出:28
解释:最大运算结果是 5 XOR 25 = 28.

提示:

  • 1 <= nums.length <= 2 * 104
  • 0 <= nums[i] <= 231 - 1

思路:

前缀树:
每个节点有 2 个分支:我们把每个元素看成一个 32 位的 01 串(数值不足 在前面补 0),将 32 位二进制串插入一棵 trie 树(从高位开始插入)。
insert:由于我们要求两个元素的异或最大值,所以我们肯定是要从最高位开始考虑的,因此我们存 x 时从左向右开始存储在 trie 中。
search:我们先在 trie 树中找到能与 x 异或取得最大值的另一个数组元素 y,因为异或的运算的法则是相同得0,不同得1,所以我们尽可能走与 x 当前位相反的字符方向走,才能得到能和 x 产生最大值的另一个数组元素 y,然后 res=x^y

class Trie {
public:
    vector<Trie*> next;
    Trie():next(2,nullptr){}
    void insert(int x) {
        Trie* node = this;
        for(int i = 30; i >= 0; i--) {
            // 取x的第i位
            int u = (x >> i) & 1;
            if(node->next[u] == nullptr) {
                node->next[u] = new Trie();
            }
            node = node->next[u];
        }
    }
    int search(int x) {
        Trie* node = this;
        // 存储 x 能够异或的最大值
        int res = 0;
        for(int i = 30; i >= 0; i--) {
            int u = (x >> i) & 1;
            // 优先选择取该位反方向为最大 因为 1 ^ 0 = 1
            if(node->next[!u]) {
                node = node->next[!u];
                res = (res << 1) ^ !u;
            }else {
                // 没有的话取本身
                node = node->next[u];
                res = (res << 1) ^ u;
            }
        }
        // ans = res ^ x
        res ^= x;
        return res;
    }
};

class Solution {
public:
    int findMaximumXOR(vector<int>& nums) {
        Trie* node = new Trie();
        for(int i : nums) {
            node->insert(i);
        }
        int res = 0;
        for(int i : nums) {
            res = max(res, node->search(i));
        }
        return res;
    }
};
  • 27
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 40
    评论
评论 40
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只小逸白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值