题目描述

✅ LCR 064. 实现一个魔法字典

image-20260929010524218

image-20260929010524219

题意分析

先用词库建好字典,再判断每个查询串能否恰好替换一个字符后变成某个词库单词。不能增加或删除字符,替换后的字符也必须与原字符不同,所以目标单词需要与查询串长度相同,并且只在一个位置上不同。

查询串本身在词库中,并不直接说明成功或失败:它与自己没有差异,但词库里仍可能存在另一个只差一位的单词。题目保证词库内的单词互不相同,且 buildDict 只在查询前调用一次。

解法:单字符通配签名与计数

核心思路

[!blue]

两个等长单词如果只在位置 i 不同,将该位置都替换为 * 后,就会得到相同字符串。把这个只含一个 * 的字符串称为模式。因为输入仅有小写字母,* 不会与原字符混淆;相同模式还会同时保证字符串长度和被遮住的位置一致。

共享模式只能说明两个单词在其余位置完全相同,也就是至多差一个字符,不能直接推出恰好差一个。因此维护两张表:counter[p] 记录多少个词库单词能生成模式 p,words 保存完整的词库单词,用来判断查询串是否贡献了其中的一份计数。

建库时,每个长度为 L 的单词依次遮住各个位置,生成 L 个模式并累加计数。词库单词互不相同,同一单词不同位置的星号也不同,所以 counter[p] 正好是共享该模式的不同词库单词数。

查询时生成同样的模式,并检查每个模式的计数 cnt:

  • cnt > 1:至少有两个不同词库单词共享该模式,其中必有一个不是查询串。其余位置相同、整串又不同,差异就只能在被遮住的那个位置,满足恰好替换一次。
  • cnt == 1 且查询串不在 words 中:唯一匹配的词库单词也不可能是查询串自身,同样恰好差一位。
  • cnt == 0,或 cnt == 1 且查询串在词库中:没有可用的其他单词,这个模式不能判定成功,应继续检查其他位置。

任意模式成功就返回真,全部失败才返回假。若确实存在只差一位的词库单词,遮住那个差异位置一定会遇到它,因此不会漏解;长度不同的单词生成的模式长度也不同,天然无法匹配。

Java 通过字符数组临时改一位、创建模式后立即还原,保证下一次仍只遮住一个位置;Go 每次用前缀、*、后缀拼成新模式,实现相同的生成规则。

解题步骤

  1. 初始化完整词集合 words 和模式计数表 counter。
  2. 建库时保存每个原词,并逐位生成单星模式、增加对应计数。
  3. 查询时按同样方式生成各位置的模式,读取 cnt。
  4. 当 cnt > 1,或 cnt == 1 且查询串不在原词集合中时返回真;所有位置都不满足则返回假。

代码实现

class MagicDictionary {
    private Set<String> words;
    private Map<String, Integer> counter;

    public MagicDictionary() {
        words = new HashSet<>();
        counter = new HashMap<>();
    }

    public void buildDict(String[] dictionary) {
        for (String word : dictionary) {
            words.add(word);

            for (String p : patterns(word)) {
                counter.put(p, counter.getOrDefault(p, 0) + 1);
            }
        }
    }

    public boolean search(String searchWord) {
        for (String p : patterns(searchWord)) {
            int cnt = counter.getOrDefault(p, 0);

            if (cnt > 1 || (cnt == 1 && !words.contains(searchWord))) {
                return true;
            }
        }

        return false;
    }

    private List<String> patterns(String word) {
        List<String> res = new ArrayList<>();
        char[] chars = word.toCharArray();

        for (int i = 0; i < chars.length; ++i) {
            char c = chars[i];

            chars[i] = '*';
            res.add(new String(chars));
            chars[i] = c;
        }

        return res;
    }
}
type MagicDictionary struct {
    words   map[string]bool
    counter map[string]int
}

func Constructor() MagicDictionary {
    return MagicDictionary{
        words:   make(map[string]bool),
        counter: make(map[string]int),
    }
}

func (this *MagicDictionary) BuildDict(dictionary []string) {
    for _, word := range dictionary {
        this.words[word] = true
        for _, p := range patterns(word) {
            this.counter[p]++
        }
    }
}

func (this *MagicDictionary) Search(searchWord string) bool {
    for _, p := range patterns(searchWord) {
        if this.counter[p] > 1 || (this.counter[p] == 1 && !this.words[searchWord]) {
            return true
        }
    }
    return false
}

func patterns(word string) []string {
    var res []string
    for i := 0; i < len(word); i++ {
        res = append(res, word[:i]+"*"+word[i+1:])
    }
    return res
}

复杂度分析

  • 时间复杂度:设词库含 N 个单词、最大长度为 L。建库期望为 $O(NL^2)$:每词生成至多 L 个模式,模式构造与哈希按长度计费。长度为 P 的单次查询期望为 $O(P^2)$,与词库单词数量无关。
  • 空间复杂度:持久存储最坏为 $O(NL^2)$,至多保存 NL 个长度不超过 L 的模式,另有原词集合。当前 patterns 一次性生成全部模式,单次查询还需要 $O(P^2)$ 临时空间。

关键点总结

[!green]

  • 单星模式固定了长度、差异位置和其余全部字符,只把可能变化的一位隐藏起来。
  • 模式存在只能证明至多差一位;计数与原词集合一起排除零次修改。
  • 查询原词时不能直接拒绝,还要检查是否有另一单词共享某个模式。
  • 单次建库和词库互异的题面保证,使模式计数可以直接按单词累加。

易错点总结

[!yellow]

  • 只记录模式是否存在,会把查询串与自身的零差异误判为一次替换。
  • 因为查询原词已存在就直接返回假,会漏掉它与其他词只差一位的情况。
  • Java 生成一个模式后不还原字符,会让后续模式含多个星号,错误允许多处变化。
  • 占位符必须在输入字母表之外,且建库与查询使用完全相同的生成规则。

相似题目

题目 难度 关联与区别
211. 添加与搜索单词 - 数据结构设计 中等 同样在Trie中进行带分支的匹配,原题通配符位置由模式给定,本题自行消耗恰好一次替换。
161. 相隔为 1 的编辑距离 中等 都限制恰好一次编辑,原题允许插入删除,本题只能替换一个字符且需命中字典。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/59912056
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!