题目分析
问题描述
我们有一个 L×CL \times CL×C 的字母矩阵,以及 WWW 个需要查找的单词。每个单词在矩阵中会以直线方向出现(共 888 个可能方向)。需要找出每个单词在矩阵中第一次出现的起始位置和方向。
方向编码规则
题目使用字母 A\texttt{A}A 到 H\texttt{H}H 表示 888 个方向,从正北开始顺时针排列:
- A\texttt{A}A: 北 (−1,0)(-1, 0)(−1,0)
- B\texttt{B}B: 东北 (−1,+1)(-1, +1)(−1,+1)
- C\texttt{C}C: 东 (0,+1)(0, +1)(0,+1)
- D\texttt{D}D: 东南 (+1,+1)(+1, +1)(+1,+1)
- E\texttt{E}E: 南 (+1,0)(+1, 0)(+1,0)
- F\texttt{F}F: 西南 (+1,−1)(+1, -1)(+1,−1)
- G\texttt{G}G: 西 (0,−1)(0, -1)(0,−1)
- H\texttt{H}H: 西北 (−1,−1)(-1, -1)(−1,−1)
输入输出格式
输入格式:
- 第一行:测试用例数
- 每个测试用例:
- 第一行:LLL CCC WWW(行数、列数、单词数)
- 接下来 LLL 行:字母矩阵
- 接下来 WWW 行:要查找的单词
输出格式:
- 每个单词一行:起始行 起始列 方向字母
- 测试用例之间用空行分隔
数据范围
- 0<L≤10000 < L \leq 10000<L≤1000
- 0<C≤10000 < C \leq 10000<C≤1000
- 0<W≤10000 < W \leq 10000<W≤1000
解题思路
暴力搜索的局限性
最直接的解法是从矩阵的每个位置出发,向 888 个方向尝试匹配每个单词。这种暴力方法的时间复杂度为 O(L×C×W×len×8)O(L \times C \times W \times \texttt{len} \times 8)O(L×C×W×len×8),在数据范围上限时无法通过。
优化方案:Trie\texttt{Trie}Trie 树 + 方向搜索
我们采用**Trie\texttt{Trie}Trie 树(字典树)**来优化搜索过程:
-
构建 Trie\texttt{Trie}Trie 树:将所有待查找单词插入Trie\texttt{Trie}Trie 树中,每个叶子节点记录对应单词的索引。
-
矩阵搜索:从矩阵的每个位置 (r,c)(r, c)(r,c) 出发,向 888 个方向在 Trie\texttt{Trie}Trie 树上行走:
- 沿着当前方向取字符
- 在 Trie\texttt{Trie}Trie 树上移动对应分支
- 如果到达单词结尾节点,记录答案
-
提前终止:一旦所有单词都找到,立即停止搜索。
算法优势
- 时间复杂度:O(L×C×8×maxLength)O(L \times C \times 8 \times \text{maxLength})O(L×C×8×maxLength),其中 maxLength\text{maxLength}maxLength 是最长单词长度
- 空间复杂度:O(总单词长度×26)O(\text{总单词长度} \times 26)O(总单词长度×26)
- 实际效率:比暴力搜索快很多,因为 Trie\texttt{Trie}Trie 树能快速排除不匹配的分支
代码实现
// 1127
// UVa ID: Word Puzzles
// Verdict: Accepted
// Submission Date: 2025-10-16
// UVa Run Time: 0.300s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <iostream>
#include <vector>
#include <string>
#include <cstring>
#include <tuple>
using namespace std;
const int MAXN = 1000;
const int ALPHABET = 26;
// 8个方向:北、东北、东、东南、南、西南、西、西北
int dr[8] = {-1, -1, 0, 1, 1, 1, 0, -1};
int dc[8] = {0, 1, 1, 1, 0, -1, -1, -1};
char dirChar[8] = {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'};
// Trie树节点结构
struct TrieNode {
int wordIndex; // -1表示不是单词结尾,否则存储单词在words中的下标
TrieNode* children[ALPHABET];
TrieNode() : wordIndex(-1) {
memset(children, 0, sizeof(children));
}
};
// 向Trie树中插入单词
void insertWord(TrieNode* root, const string& word, int index) {
TrieNode* node = root;
for (char ch : word) {
int idx = ch - 'A';
if (!node->children[idx]) {
node->children[idx] = new TrieNode();
}
node = node->children[idx];
}
node->wordIndex = index;
}
void solve() {
int L, C, W;
cin >> L >> C >> W;
vector<string> grid(L);
for (int i = 0; i < L; i++) {
cin >> grid[i];
}
vector<string> words(W);
for (int i = 0; i < W; i++) {
cin >> words[i];
}
// 构建Trie树
TrieNode* root = new TrieNode();
for (int i = 0; i < W; i++) {
insertWord(root, words[i], i);
}
// 存储每个单词的答案,初始化为未找到状态
vector<tuple<int, int, char>> ans(W);
for (int i = 0; i < W; i++) {
ans[i] = make_tuple(-1, -1, ' ');
}
int foundCount = 0; // 已找到的单词数量,用于提前终止
// 从每个位置每个方向在Trie树上搜索
for (int r = 0; r < L && foundCount < W; r++) {
for (int c = 0; c < C && foundCount < W; c++) {
for (int dir = 0; dir < 8 && foundCount < W; dir++) {
TrieNode* node = root;
int nr = r, nc = c;
// 沿着当前方向在Trie树上行走
while (nr >= 0 && nr < L && nc >= 0 && nc < C && node) {
char ch = grid[nr][nc];
int idx = ch - 'A';
node = node->children[idx];
if (!node) break; // Trie树上无此分支,提前结束
// 如果找到一个单词
if (node->wordIndex != -1) {
int wi = node->wordIndex;
if (get<0>(ans[wi]) == -1) { // 只记录第一次出现
ans[wi] = make_tuple(r, c, dirChar[dir]);
foundCount++;
}
}
nr += dr[dir];
nc += dc[dir];
}
}
}
}
// 输出结果
for (int i = 0; i < W; i++) {
cout << get<0>(ans[i]) << " " << get<1>(ans[i]) << " " << get<2>(ans[i]) << endl;
}
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int t;
cin >> t;
for (int i = 0; i < t; i++) {
if (i > 0) cout << endl;
solve();
}
return 0;
}
关键点说明
-
Trie树构建:将单词按字符顺序插入,形成前缀树结构。
-
方向处理:使用方向数组 dr\texttt{dr}dr 和 dc\texttt{dc}dc 统一处理 888 个方向的移动。
-
提前终止:通过 foundCount\texttt{foundCount}foundCount 变量在所有单词找到后立即停止搜索,大幅提升效率。
-
只记录第一次出现:使用 -1\texttt{-1}-1 初始值确保只记录每个单词的第一次出现位置。
该算法充分利用了Trie树的前缀匹配特性,避免了大量不必要的比较,是解决此类单词搜索问题的高效方法。
2345

被折叠的 条评论
为什么被折叠?



