Article / 文章

LeetCode 第425题:单词方阵

给定一个单词集合 words,其中所有单词长度都相同。找出并返回其可以构成的所有单词方阵。 单词方阵是一个 n × n 的方阵,由单词集合中的单词构成,并满足: - 如果我们将这个方阵逐行读出,将会得到单词集合中的某些单词。 - 如果我们将这个方阵逐列读出,将会得到单词集合中的某些单词。

📖 文章摘要

本文详细解析LeetCode第425题“单词方阵”,这是一道涉及字符串匹配和回溯算法的问题。文章提供了基于字典树(Trie)和回溯的解题思路,包含C#、Python、C++三种语言实现,配有详细的分析和性能对比。适合想要提升字符串处理和回溯算法能力的程序员。

核心知识点: 字典树、回溯算法、字符串处理
难度等级: 困难
推荐人群: 具有中级算法基础的程序员

题目描述

给定一个单词集合 words,其中所有单词长度都相同。找出并返回其可以构成的所有单词方阵。

单词方阵是一个 n × n 的方阵,由单词集合中的单词构成,并满足:

  • 如果我们将这个方阵逐行读出,将会得到单词集合中的某些单词。
  • 如果我们将这个方阵逐列读出,将会得到单词集合中的某些单词。

示例

示例 1:

输入:words = ["area","lead","wall","lady","ball"]
输出:[
  [ "wall",
    "area",
    "lead",
    "lady"
  ],
  [ "ball",
    "area",
    "lead",
    "lady"
  ]
]
解释:
输出包含两个单词方阵,它们都符合题目要求。

示例 2:

输入:words = ["abat","baba","atan","atal"]
输出:[
  [ "baba",
    "abat",
    "baba",
    "atan"
  ],
  [ "baba",
    "abat",
    "baba",
    "atal"
  ]
]

提示

  • 1 <= words.length <= 1000
  • 1 <= words[i].length <= 5
  • words[i] 由小写英文字母组成
  • words 中的所有单词长度相同

解题思路

本题可以使用字典树(Trie)和回溯算法来解决。主要步骤如下:

  1. 构建字典树:

    • 将所有单词插入字典树
    • 支持按前缀查找单词的功能
  2. 回溯构建方阵:

    • 逐行填充单词
    • 检查每一列是否构成有效前缀
    • 如果当前行有效,继续填充下一行
    • 找到完整解时保存结果

图解思路

字典树结构分析表

组件 功能 实现方式
节点 存储字符 字符数组
子节点 指向下一个字符 哈希表/数组
单词标记 标记完整单词 布尔值
前缀查找 获取具有相同前缀的单词 深度优先搜索

回溯过程分析表

步骤 操作 检查项 后续处理
1 选择单词 长度匹配 继续或回溯
2 检查列前缀 是否有效 继续或回溯
3 填充下一行 递归处理 保存或回溯
4 完成方阵 全部匹配 保存结果

代码实现

C# 实现

public class Solution {
    class TrieNode {
        public Dictionary<char, TrieNode> children;
        public List<string> words;
        
        public TrieNode() {
            children = new Dictionary<char, TrieNode>();
            words = new List<string>();
        }
    }
    
    private TrieNode root;
    
    public IList<IList<string>> WordSquares(string[] words) {
        // 构建字典树
        BuildTrie(words);
        
        var result = new List<IList<string>>();
        var n = words[0].Length;
        
        // 对每个单词尝试作为第一行
        foreach (var word in words) {
            var square = new List<string> { word };
            Backtrack(n, square, result);
        }
        
        return result;
    }
    
    private void BuildTrie(string[] words) {
        root = new TrieNode();
        
        foreach (var word in words) {
            var node = root;
            // 将单词的每个前缀都记录下来
            for (int i = 0; i < word.Length; i++) {
                var c = word[i];
                if (!node.children.ContainsKey(c)) {
                    node.children[c] = new TrieNode();
                }
                node = node.children[c];
                node.words.Add(word);
            }
        }
    }
    
    private void Backtrack(int n, List<string> square, List<IList<string>> result) {
        if (square.Count == n) {
            result.Add(new List<string>(square));
            return;
        }
        
        int row = square.Count;
        var prefix = new StringBuilder();
        // 获取下一行的前缀
        for (int i = 0; i < row; i++) {
            prefix.Append(square[i][row]);
        }
        
        // 查找具有相同前缀的单词
        foreach (var word in FindWordsWithPrefix(prefix.ToString())) {
            square.Add(word);
            Backtrack(n, square, result);
            square.RemoveAt(square.Count - 1);
        }
    }
    
    private List<string> FindWordsWithPrefix(string prefix) {
        var node = root;
        foreach (var c in prefix) {
            if (!node.children.ContainsKey(c)) {
                return new List<string>();
            }
            node = node.children[c];
        }
        return node.words;
    }
}

Python 实现

class Solution:
    def wordSquares(self, words: List[str]) -> List[List[str]]:
        # 构建前缀字典
        prefix_dict = collections.defaultdict(list)
        n = len(words[0])
        
        for word in words:
            for i in range(n):
                prefix_dict[word[:i]].append(word)
        
        def backtrack(square):
            if len(square) == n:
                result.append(square[:])
                return
            
            # 获取下一行的前缀
            prefix = ''.join(word[len(square)] for word in square)
            # 查找具有相同前缀的单词
            for word in prefix_dict[prefix]:
                square.append(word)
                backtrack(square)
                square.pop()
        
        result = []
        for word in words:
            backtrack([word])
        return result

C++ 实现

class Solution {
    struct TrieNode {
        unordered_map<char, TrieNode*> children;
        vector<string> words;
        
        TrieNode() {}
    };
    
    TrieNode* root;
    
public:
    vector<vector<string>> wordSquares(vector<string>& words) {
        // 构建字典树
        buildTrie(words);
        
        vector<vector<string>> result;
        int n = words[0].length();
        
        // 对每个单词尝试作为第一行
        for (const string& word : words) {
            vector<string> square = {word};
            backtrack(n, square, result);
        }
        
        return result;
    }
    
private:
    void buildTrie(const vector<string>& words) {
        root = new TrieNode();
        
        for (const string& word : words) {
            TrieNode* node = root;
            // 将单词的每个前缀都记录下来
            for (int i = 0; i < word.length(); i++) {
                char c = word[i];
                if (node->children.find(c) == node->children.end()) {
                    node->children[c] = new TrieNode();
                }
                node = node->children[c];
                node->words.push_back(word);
            }
        }
    }
    
    void backtrack(int n, vector<string>& square, vector<vector<string>>& result) {
        if (square.size() == n) {
            result.push_back(square);
            return;
        }
        
        int row = square.size();
        string prefix;
        // 获取下一行的前缀
        for (int i = 0; i < row; i++) {
            prefix += square[i][row];
        }
        
        // 查找具有相同前缀的单词
        for (const string& word : findWordsWithPrefix(prefix)) {
            square.push_back(word);
            backtrack(n, square, result);
            square.pop_back();
        }
    }
    
    vector<string> findWordsWithPrefix(const string& prefix) {
        TrieNode* node = root;
        for (char c : prefix) {
            if (node->children.find(c) == node->children.end()) {
                return vector<string>();
            }
            node = node->children[c];
        }
        return node->words;
    }
};

执行结果

C# 实现

  • 执行用时:296 ms
  • 内存消耗:52.8 MB

Python 实现

  • 执行用时:168 ms
  • 内存消耗:16.2 MB

C++ 实现

  • 执行用时:48 ms
  • 内存消耗:24.6 MB

性能对比

语言 执行用时 内存消耗 特点
C# 296 ms 52.8 MB 实现清晰但性能较差
Python 168 ms 16.2 MB 代码简洁,性能中等
C++ 48 ms 24.6 MB 性能最优,内存适中

代码亮点

  1. 🎯 使用字典树优化前缀查找
  2. 💡 回溯算法高效剪枝
  3. 🔍 空间优化的前缀存储
  4. 🎨 模块化设计,代码复用

常见错误分析

  1. 🚫 忽略列检查导致无效方阵
  2. 🚫 字典树内存泄漏
  3. 🚫 回溯状态未正确恢复
  4. 🚫 前缀查找效率低下

解法对比

解法 时间复杂度 空间复杂度 优点 缺点
字典树+回溯 O(N * 26^L) O(N * L) 效率高,易于实现 内存消耗大
暴力枚举 O(N^L) O(L) 实现简单 性能很差

相关题目


📖 系列导航

🔥 算法专题合集 - 查看完整合集

📢 关注合集更新:点击上方合集链接,关注获取最新题解!目前已更新第425题。


💬 互动交流

感谢大家耐心阅读到这里!希望这篇题解能够帮助你更好地理解和掌握这道算法题。

如果这篇文章对你有帮助,请:

  • 👍 点个赞,让更多人看到这篇文章
  • 📁 收藏文章,方便后续查阅复习
  • 🔔 关注作者,获取更多高质量算法题解
  • 💭 评论区留言,分享你的解题思路或提出疑问

你的支持是我持续分享的动力!

💡 一起进步:算法学习路上不孤单,欢迎一起交流学习!