LeetCode 2452. 距离字典两次编辑以内的单词【暴力;字典树】中等

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

给你两个字符串数组 queriesdictionary 。数组中所有单词都只包含小写英文字母,且长度都相同。

一次 编辑 中,你可以从 queries 中选择一个单词,将任意一个字母修改成任何其他字母。从 queries 中找到所有满足以下条件的字符串:不超过 两次编辑内,字符串与 dictionary 中某个字符串相同。

请你返回 queries 中的单词列表,这些单词距离 dictionary 中的单词 编辑次数 不超过 两次 。单词返回的顺序需要与 queries 中原本顺序相同。

示例 1:

java 复制代码
输入:queries = ["word","note","ants","wood"], dictionary = ["wood","joke","moat"]
输出:["word","note","wood"]
解释:
- 将 "word" 中的 'r' 换成 'o' ,得到 dictionary 中的单词 "wood" 。
- 将 "note" 中的 'n' 换成 'j' 且将 't' 换成 'k' ,得到 "joke" 。
- "ants" 需要超过 2 次编辑才能得到 dictionary 中的单词。
- "wood" 不需要修改(0 次编辑),就得到 dictionary 中相同的单词。
所以我们返回 ["word","note","wood"] 。

示例 2:

java 复制代码
输入:queries = ["yes"], dictionary = ["not"]
输出:[]
解释:
"yes" 需要超过 2 次编辑才能得到 "not" 。
所以我们返回空数组。

提示:

  • 1 <= queries.length, dictionary.length <= 100
  • n == queries[i].length == dictionary[j].length
  • 1 <= n <= 100
  • 所有 queries[i]dictionary[j] 都只包含小写英文字母。

方法1 暴力

对每个 q = q u e r i e s [ i ] q = queries[i] q=queries[i] ,遍历 d i c t i o n a r y dictionary dictionary 中的字符串 s s s ,判断 q , s q, s q,s 是否至多有两个位置的字母不同。

java 复制代码
class Solution {
    public List<String> twoEditWords(String[] queries, String[] dictionary) {
        List<String> ans = new ArrayList<>();
        for (String q : queries) {
            for (String s : dictionary) {
                int cnt = 0;
                for (int i = 0; i < s.length() && cnt <= 2; i++) {
                    if (q.charAt(i) != s.charAt(i)) {
                        cnt++;
                    }
                }
                if (cnt <= 2) {
                    ans.add(q);
                    break;
                }
            }
        }
        return ans;
    }
}
cpp 复制代码
class Solution {
public:
    vector<string> twoEditWords(vector<string>& queries, vector<string>& dictionary) {
        vector<string> ans;
        for (auto& q : queries) {
            for (auto& s : dictionary) {
                int cnt = 0;
                for (int i = 0; i < s.size() && cnt <= 2; i++) {
                    if (q[i] != s[i]) {
                        cnt++;
                    }
                }
                if (cnt <= 2) {
                    ans.push_back(q);
                    break;
                }
            }
        }
        return ans;
    }
};
python 复制代码
class Solution:
    def twoEditWords(self, queries: List[str], dictionary: List[str]) -> List[str]:
        ans = []
        for q in queries:
            for s in dictionary:
                if sum(x != y for x, y in zip(q, s)) <= 2:
                    ans.append(q)
                    break
        return ans
rust 复制代码
impl Solution {
    pub fn two_edit_words(queries: Vec<String>, dictionary: Vec<String>) -> Vec<String> {
        let mut ans = vec![];
        for q in queries {
            for s in &dictionary {
                let mut cnt = 0;
                for (a, b) in q.bytes().zip(s.bytes()) {
                    if a != b {
                        cnt += 1;
                        if cnt > 2 {
                            break;
                        }
                    }
                }
                if cnt <= 2 {
                    ans.push(q);
                    break;
                }
            }
        }
        ans
    }
}
go 复制代码
func twoEditWords(queries, dictionary []string) (ans []string) {
	for _, q := range queries {
	next:
		for _, s := range dictionary {
			cnt := 0
			for i := range s {
				if q[i] != s[i] {
					cnt++
					if cnt > 2 {
						continue next
					}
				}
			}
			ans = append(ans, q)
			break
		}
	}
	return
}
js 复制代码
var twoEditWords = function(queries, dictionary) {
    const ans = [];
    for (const q of queries) {
        for (const s of dictionary) {
            let cnt = 0;
            for (let i = 0; i < s.length && cnt <= 2; i++) {
                if (q[i] !== s[i]) {
                    cnt++;
                }
            }
            if (cnt <= 2) {
                ans.push(q);
                break;
            }
        }
    }
    return ans;
};
c 复制代码
char** twoEditWords(char** queries, int queriesSize, char** dictionary, int dictionarySize, int* returnSize) {
    char** ans = malloc(queriesSize * sizeof(char*));
    *returnSize = 0;

    for (int i = 0; i < queriesSize; i++) {
        char* q = queries[i];
        for (int j = 0; j < dictionarySize; j++) {
            char* s = dictionary[j];
            int cnt = 0;
            for (int k = 0; s[k] && cnt <= 2; k++) {
                if (q[k] != s[k]) {
                    cnt++;
                }
            }
            if (cnt <= 2) {
                ans[(*returnSize)++] = q;
                break;
            }
        }
    }

    return ans;
}

复杂度分析:

  • 时间复杂度: O ( q d n ) O(qdn) O(qdn) , q q q 是 q u e r i e s queries queries 的长度, d d d 是 d i c t i o n a r y dictionary dictionary 的长度, n n n 是 q u e r i e s [ i ] queries[i] queries[i] 的长度。题目保证所有字符串长度相等。
  • 空间复杂度: O ( 1 ) O(1) O(1) 。返回值不计入。

方法2 字典树

可将 d i c t i o n a r y dictionary dictionary 中所有的单词插入字典树,对每个 q u e r i e s [ i ] queries[i] queries[i] 做DFS,过程中维护修改次数 c n t cnt cnt ,从而实现字典树上【最多 2 2 2 次修改】的匹配搜索。

定义状态 d f s ( i , n o d e , c n t ) dfs(i, node, cnt) dfs(i,node,cnt) , i i i 表示当前匹配到第 i i i 个字符, n o d e node node 表示当前所在字典树的节点, c n t cnt cnt 表示已经修改 c n t cnt cnt 次。对第 i i i 个字符 q u e r i e s [ i ] queries[i] queries[i] :

  1. 如果字典树中存在 n o d e . c h i l d r e n [ q u e r i e s [ i ] ] node.children[queries[i]] node.children[queries[i]] ,不进行修改。下一步搜索状态 d f s ( i + 1 , n o d e . c h i l d r e n [ q u e r i e s [ i ] ] , c n t ) dfs(i + 1, node.children[queries[i]], cnt) dfs(i+1,node.children[queries[i]],cnt) 。
  2. 如果字典树中不存在 n o d e . c h i l d r e n [ q u e r i e s [ i ] ] node.children[queries[i]] node.children[queries[i]] ,且 c n t < 2 cnt <2 cnt<2 ,进行修改,枚举所有 c ≠ q u e r i e s [ i ] c \ne queries[i] c=queries[i] ,下一步搜索状态 d f s ( i + 1 , n o d e . c h i l d r e n [ c ] , c n t + 1 ) dfs(i + 1, node.children[c], cnt+1) dfs(i+1,node.children[c],cnt+1) 。

搜索过程中,我们可进行一些剪枝,比如某条路径找到合法解就提前终止。

java 复制代码
class Solution {
    static class TrieNode {
        TrieNode[] child = new TrieNode[26];
        boolean isEnd = false;
    }
	    TrieNode root = new TrieNode();
    void insert(String word) {
        TrieNode node = root;
        for (char c : word.toCharArray()) {
            int idx = c - 'a';
            if (node.child[idx] == null)
                node.child[idx] = new TrieNode();
            node = node.child[idx];
        }
        node.isEnd = true;
    }
    boolean dfs(String word, int i, TrieNode node, int cnt) {
        if (cnt > 2 || node == null)
            return false;
        if (i == word.length())
            return node.isEnd;
        int idx = word.charAt(i) - 'a';

        // 不修改
        if (node.child[idx] != null) {
            if (dfs(word, i + 1, node.child[idx], cnt))
                return true;
        }
        // 修改
        if (cnt < 2) {
            for (int c = 0; c < 26; c++) {
                if (c == idx)
                    continue;
                if (node.child[c] != null) {
                    if (dfs(word, i + 1, node.child[c], cnt + 1))
                        return true;
                }
            }
        }
        return false;
    }

    public List<String> twoEditWords(String[] queries, String[] dictionary) {
        for (String w : dictionary)
            insert(w);
        List<String> res = new ArrayList<>();
        for (String q : queries) {
            if (dfs(q, 0, root, 0)) {
                res.add(q);
            }
        }
        return res;
    }
}
cpp 复制代码
struct TrieNode {
    TrieNode* child[26];
    bool isEnd;
    TrieNode() {
        memset(child, 0, sizeof(child));
        isEnd = false;
    }
};

class Solution {
public:
    TrieNode* root = new TrieNode();
    void insert(string& word) {
        TrieNode* node = root;
        for (char c : word) {
            int idx = c - 'a';
            if (!node->child[idx])
                node->child[idx] = new TrieNode();
            node = node->child[idx];
        }
        node->isEnd = true;
    }

    bool dfs(string& word, int i, TrieNode* node, int cnt) {
        if (cnt > 2)
            return false;
        if (!node)
            return false;
        if (i == word.size()) {
            return node->isEnd;
        }
        int idx = word[i] - 'a';
        // 不修改
        if (node->child[idx]) {
            if (dfs(word, i + 1, node->child[idx], cnt))
                return true;
        }
        // 修改
        if (cnt < 2) {
            for (int c = 0; c < 26; c++) {
                if (c == idx)
                    continue;
                if (node->child[c]) {
                    if (dfs(word, i + 1, node->child[c], cnt + 1))
                        return true;
                }
            }
        }
        return false;
    }

    vector<string> twoEditWords(vector<string>& queries,
                                vector<string>& dictionary) {
        for (auto& w : dictionary)
            insert(w);
        vector<string> res;
        for (auto& q : queries) {
            if (dfs(q, 0, root, 0)) {
                res.push_back(q);
            }
        }
        return res;
    }
};
python 复制代码
class TrieNode:
    def __init__(self):
        self.child = [None] * 26
        self.isEnd = False


class Solution:
    def __init__(self):
        self.root = TrieNode()

    def insert(self, word):
        node = self.root
        for c in word:
            idx = ord(c) - ord('a')
            if not node.child[idx]:
                node.child[idx] = TrieNode()
            node = node.child[idx]
        node.isEnd = True

    def dfs(self, word, i, node, cnt):
        if cnt > 2 or not node:
            return False

        if i == len(word):
            return node.isEnd

        idx = ord(word[i]) - ord('a')

        # 不修改
        if node.child[idx] and self.dfs(word, i + 1, node.child[idx], cnt):
            return True

        # 修改
        if cnt < 2:
            for c in range(26):
                if c == idx:
                    continue
                if node.child[c] and self.dfs(word, i + 1, node.child[c], cnt + 1):
                    return True

        return False

    def twoEditWords(self, queries, dictionary):
        for w in dictionary:
            self.insert(w)

        res = []
        for q in queries:
            if self.dfs(q, 0, self.root, 0):
                res.append(q)
        return res
rust 复制代码
struct TrieNode {
    child: Vec<Option<Box<TrieNode>>>,
    is_end: bool,
}

impl TrieNode {
    fn new() -> Self {
        let mut child = Vec::with_capacity(26);
        for _ in 0..26 {
            child.push(None);
        }

        Self {
            child,
            is_end: false,
        }
    }
}

impl Solution {
    fn insert(root: &mut TrieNode, word: &str) {
        let mut node = root;
        for c in word.chars() {
            let idx = (c as u8 - b'a') as usize;
            if node.child[idx].is_none() {
                node.child[idx] = Some(Box::new(TrieNode::new()));
            }
            node = node.child[idx].as_mut().unwrap();
        }
        node.is_end = true;
    }

    fn dfs(word: &[u8], i: usize, node: &TrieNode, cnt: i32) -> bool {
        if cnt > 2 {
            return false;
        }
        if i == word.len() {
            return node.is_end;
        }

        let idx = (word[i] - b'a') as usize;

        // 不修改
        if let Some(ref next) = node.child[idx] {
            if Self::dfs(word, i + 1, next, cnt) {
                return true;
            }
        }

        // 修改
        if cnt < 2 {
            for c in 0..26 {
                if c == idx {
                    continue;
                }
                if let Some(ref next) = node.child[c] {
                    if Self::dfs(word, i + 1, next, cnt + 1) {
                        return true;
                    }
                }
            }
        }

        false
    }

    pub fn two_edit_words(queries: Vec<String>, dictionary: Vec<String>) -> Vec<String> {
        let mut root = TrieNode::new();

        for w in &dictionary {
            Self::insert(&mut root, w);
        }

        let mut res = vec![];
        for q in &queries {
            if Self::dfs(q.as_bytes(), 0, &root, 0) {
                res.push(q.clone());
            }
        }

        res
    }
}
go 复制代码
type TrieNode struct {
    child [26]*TrieNode
    isEnd bool
}

var root = &TrieNode{}

func insert(word string) {
    node := root
    for _, c := range word {
        idx := c - 'a'
        if node.child[idx] == nil {
            node.child[idx] = &TrieNode{}
        }
        node = node.child[idx]
    }
    node.isEnd = true
}

func dfs(word string, i int, node *TrieNode, cnt int) bool {
    if cnt > 2 || node == nil {
        return false
    }

    if i == len(word) {
        return node.isEnd
    }

    idx := word[i] - 'a'

    // 不修改
    if node.child[idx] != nil && dfs(word, i+1, node.child[idx], cnt) {
        return true
    }

    // 修改
    if cnt < 2 {
        for c := 0; c < 26; c++ {
            if byte(c) == idx {
                continue
            }
            if node.child[c] != nil && dfs(word, i+1, node.child[c], cnt+1) {
                return true
            }
        }
    }

    return false
}

func twoEditWords(queries []string, dictionary []string) []string {
    root = &TrieNode{}
    for _, w := range dictionary {
        insert(w)
    }

    var res []string
    for _, q := range queries {
        if dfs(q, 0, root, 0) {
            res = append(res, q)
        }
    }
    return res
}
csharp 复制代码
public class Solution {
    class TrieNode {
        public TrieNode[] child = new TrieNode[26];
        public bool isEnd = false;
    }
    TrieNode root = new TrieNode();
    void Insert(string word) {
        var node = root;
        foreach (char c in word) {
            int idx = c - 'a';
            if (node.child[idx] == null)
                node.child[idx] = new TrieNode();
            node = node.child[idx];
        }
        node.isEnd = true;
    }
    bool Dfs(string word, int i, TrieNode node, int cnt) {
        if (cnt > 2 || node == null)
            return false;
        if (i == word.Length)
            return node.isEnd;
        int idx = word[i] - 'a';
        // 不修改
        if (node.child[idx] != null) {
            if (Dfs(word, i + 1, node.child[idx], cnt))
                return true;
        }
        // 修改
        if (cnt < 2) {
            for (int c = 0; c < 26; c++) {
                if (c == idx) continue;
                if (node.child[c] != null) {
                    if (Dfs(word, i + 1, node.child[c], cnt + 1))
                        return true;
                }
            }
        }
        return false;
    }
    public IList<string> TwoEditWords(string[] queries, string[] dictionary) {
        foreach (var w in dictionary)
            Insert(w);

        var res = new List<string>();
        foreach (var q in queries) {
            if (Dfs(q, 0, root, 0)) {
                res.Add(q);
            }
        }
        return res;
    }
}
js 复制代码
class TrieNode {
    constructor() {
        this.child = new Array(26).fill(null);
        this.isEnd = false;
    }
}

var twoEditWords = function(queries, dictionary) {
    const root = new TrieNode();

    function insert(word) {
        let node = root;
        for (let c of word) {
            let idx = c.charCodeAt(0) - 97;
            if (!node.child[idx]) node.child[idx] = new TrieNode();
            node = node.child[idx];
        }
        node.isEnd = true;
    }

    function dfs(word, i, node, cnt) {
        if (cnt > 2 || !node) return false;
        if (i === word.length) return node.isEnd;
        let idx = word.charCodeAt(i) - 97;
        // 修改
        if (node.child[idx] && dfs(word, i + 1, node.child[idx], cnt))
            return true;
        // 不修改
        if (cnt < 2) {
            for (let c = 0; c < 26; c++) {
                if (c === idx) continue;
                if (node.child[c] && dfs(word, i + 1, node.child[c], cnt + 1))
                    return true;
            }
        }
        return false;
    }
    for (let w of dictionary) insert(w);
    const res = [];
    for (let q of queries) {
        if (dfs(q, 0, root, 0)) res.push(q);
    }
    return res;
};
ts 复制代码
class TrieNode {
    child: (TrieNode | null)[] = new Array(26).fill(null);
    isEnd: boolean = false;
}
function twoEditWords(queries: string[], dictionary: string[]): string[] {
    const root = new TrieNode();
    function insert(word: string) {
        let node = root;
        for (const c of word) {
            const idx = c.charCodeAt(0) - 97;
            if (!node.child[idx]) node.child[idx] = new TrieNode();
            node = node.child[idx]!;
        }
        node.isEnd = true;
    }
    function dfs(word: string, i: number, node: TrieNode | null, cnt: number): boolean {
        if (cnt > 2 || !node) return false;
        if (i === word.length) return node.isEnd;
        const idx = word.charCodeAt(i) - 97;
        // 不修改
        if (node.child[idx] && dfs(word, i + 1, node.child[idx], cnt))
            return true;
        // 修改
        if (cnt < 2) {
            for (let c = 0; c < 26; c++) {
                if (c === idx) continue;
                if (node.child[c] && dfs(word, i + 1, node.child[c], cnt + 1))
                    return true;
            }
        }
        return false;
    }
    for (const w of dictionary) insert(w);
    const res: string[] = [];
    for (const q of queries) {
        if (dfs(q, 0, root, 0)) res.push(q);
    }
    return res;
}
c 复制代码
typedef struct TrieNode {
    struct TrieNode* child[26];
    bool isEnd;
} TrieNode;

TrieNode* newNode() {
    TrieNode* node = (TrieNode*)malloc(sizeof(TrieNode));
    memset(node->child, 0, sizeof(node->child));
    node->isEnd = false;
    return node;
}

void insert(TrieNode* root, char* word) {
    TrieNode* node = root;
    for (int i = 0; word[i]; i++) {
        int idx = word[i] - 'a';
        if (!node->child[idx])
            node->child[idx] = newNode();
        node = node->child[idx];
    }
    node->isEnd = true;
}

bool dfs(char* word, int i, TrieNode* node, int cnt) {
    if (cnt > 2 || !node)
        return false;
    if (word[i] == '\0')
        return node->isEnd;
    int idx = word[i] - 'a';
    // 不修改
    if (node->child[idx] && dfs(word, i + 1, node->child[idx], cnt))
        return true;
    // 修改
    if (cnt < 2) {
        for (int c = 0; c < 26; c++) {
            if (c == idx) continue;
            if (node->child[c] && dfs(word, i + 1, node->child[c], cnt + 1))
                return true;
        }
    }
    return false;
}

char** twoEditWords(char** queries, int queriesSize,
                    char** dictionary, int dictionarySize,
                    int* returnSize) {
    TrieNode* root = newNode();
    for (int i = 0; i < dictionarySize; i++)
        insert(root, dictionary[i]);
    char** res = (char**)malloc(sizeof(char*) * queriesSize);
    int cnt = 0;
    for (int i = 0; i < queriesSize; i++) {
        if (dfs(queries[i], 0, root, 0)) {
            res[cnt++] = queries[i];
        }
    }
    *returnSize = cnt;
    return res;
}

复杂度分析:

  • 时间复杂度: O ( d n + q n 2 ⋅ 25 2 ) O(dn + qn^2 \cdot 25^2) O(dn+qn2⋅252) , q q q 是 q u e r i e s queries queries 的长度, d d d 是 d i c t i o n a r y dictionary dictionary 的长度, n n n 是 q u e r i e s [ i ] queries[i] queries[i] 的长度。建字典树需要 O ( d n ) O(dn) O(dn) ,查询时对每个字母都有修改和不修改两种选择,选择修改位置有 C n 2 = n 2 C^2_n = n^2 Cn2=n2 种选择,其中不修改有 1 1 1 条分支,修改有 25 25 25 条分支,最多修改两次,所以有 25 2 25^2 252 种选择。
  • 空间复杂度: O ( d n ) O(dn) O(dn) ,即为字典树占用的空间。
相关推荐
王老师青少年编程2 小时前
csp信奥赛C++高频考点专项训练之贪心算法 --【排序贪心】:魔法
c++·算法·贪心·csp·信奥赛·排序贪心·魔法
wearegogog1232 小时前
基于和差波束法的单脉冲测角MATLAB实现
人工智能·算法·matlab
AI科技星2 小时前
灵魂商数(SQ) · 全域数学统一定义【乖乖数学】
算法·机器学习·数学建模·数据挖掘·量子计算
晓觉儿2 小时前
【GPLT】2026年第十一届团队程序设计天梯赛赛后题解(已写2h,存档中)
数据结构·c++·算法·深度优先·图论
We་ct2 小时前
LeetCode 322. 零钱兑换:动态规划入门实战
前端·算法·leetcode·typescript·动态规划
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 394. 字符串解码 | C++ 单栈回压法
c++·算法·leetcode
穿条秋裤到处跑3 小时前
每日一道leetcode(2026.04.22):距离字典两次编辑以内的单词
算法·leetcode
淘矿人3 小时前
Claude辅助算法设计与优化
人工智能·python·算法·microsoft·github·bug·pygame
流年如夢3 小时前
自定义类型进阶:联合与枚举
java·c语言·开发语言·数据结构·数据库·c++·算法