题目分析
本题要求我们计算在使用特定自动补全规则的手机输入法时,输入字典中所有单词所需的平均击键次数。输入法的自动补全规则如下:
- 第一个字母必须手动输入。
- 如果已输入的前缀在所有匹配的字典单词中下一个字母都相同,则系统自动输入该字母。
- 否则,系统等待用户手动输入。
关键观察
对于任意单词 w w w,其击键次数的计算方式为:
- 第一个字母总是需要手动输入(计 1 1 1 次)
- 对于后续的每个位置 i i i( i ≥ 1 i \geq 1 i≥1),如果前缀 w [ 0.. i − 1 ] w[0..i-1] w[0..i−1] 满足以下任一条件,则 w [ i ] w[i] w[i] 需要手动输入:
- 该前缀是字典中某个完整单词的结尾
- 该前缀在字典中有多个可能的下一个字母
例子分析
以字典 {hello, hell, heaven, goodbye}
为例:
hello
:需要 3 3 3 次击键(h
、l
、o
)hell
:需要 2 2 2 次击键(h
、l
)heaven
:需要 2 2 2 次击键(h
、a
)goodbye
:需要 1 1 1 次击键(g
)
总计 8 8 8 次击键,平均 2.00 2.00 2.00。
数据结构选择
由于需要高效地处理字符串前缀查询,我们选择 Trie \texttt{Trie} Trie(字典树) 作为数据结构:
- 每个节点存储 26 26 26 个子节点指针(对应小写字母)
- 记录子节点数量
- 标记当前节点是否是某个单词的结尾
算法步骤
- 构建 Trie \texttt{Trie} Trie:将所有单词插入到 Trie 中
- 计算击键次数 :对于每个单词:
- 第一个字母总是手动输入(计数 + 1 +1 +1)
- 对于后续每个字母,检查前一个字母对应的 Trie \texttt{Trie} Trie 节点:
- 如果该节点是单词结尾 或 有多个子节点,则当前字母需要手动输入
- 计算平均值:总击键次数除以单词数量,保留两位小数
复杂度分析
- 时间复杂度 : O ( L ) O(L) O(L),其中 L L L 是所有单词的总长度(不超过 1 0 6 10^6 106)
- 空间复杂度 : O ( 26 × L ) O(26 \times L) O(26×L), Trie \texttt{Trie} Trie 节点的空间开销
代码实现
cpp
// Cellphone Typing
// UVa ID: 12526
// Verdict: Accepted
// Submission Date: 2025-10-21
// UVa Run Time: 0.290s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
const int ALPHABET = 26; // 小写字母数量
struct TrieNode {
TrieNode* children[ALPHABET]; // 子节点指针数组
int childCount; // 子节点数量
bool isEndOfWord; // 标记当前节点是否是单词结尾
TrieNode() {
memset(children, 0, sizeof(children)); // 初始化子节点为空
childCount = 0;
isEndOfWord = false;
}
};
// 向 Trie 中插入单词
void insert(TrieNode* root, const string& word) {
TrieNode* node = root;
for (char ch : word) {
int idx = ch - 'a'; // 计算字母对应的索引
if (!node->children[idx]) {
node->children[idx] = new TrieNode();
node->childCount++; // 新增子节点,计数增加
}
node = node->children[idx];
}
node->isEndOfWord = true; // 标记单词结尾
}
// 计算输入单个单词所需的击键次数
int countKeystrokes(TrieNode* root, const string& word) {
int keystrokes = 1; // 第一个字母必须手动输入
TrieNode* node = root->children[word[0] - 'a']; // 移动到第一个字母对应的节点
for (size_t i = 1; i < word.length(); i++) {
// 如果当前节点是单词结尾或有多个子节点,需要手动输入下一个字母
if (node->childCount > 1 || node->isEndOfWord) {
keystrokes++;
}
node = node->children[word[i] - 'a']; // 移动到下一个字母的节点
}
return keystrokes;
}
// 递归释放 Trie 内存
void clearTrie(TrieNode* node) {
for (int i = 0; i < ALPHABET; i++) {
if (node->children[i]) {
clearTrie(node->children[i]);
delete node->children[i];
}
}
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
int N;
while (cin >> N) {
vector<string> words(N);
TrieNode* root = new TrieNode();
// 读取所有单词并构建 Trie
for (int i = 0; i < N; i++) {
cin >> words[i];
insert(root, words[i]);
}
// 计算总击键次数
int totalKeystrokes = 0;
for (const string& w : words) {
totalKeystrokes += countKeystrokes(root, w);
}
// 计算并输出平均值
double average = static_cast<double>(totalKeystrokes) / N;
cout << fixed << setprecision(2) << average << "\n";
// 释放 Trie 内存
clearTrie(root);
delete root;
}
return 0;
}
该解法通过 Trie \texttt{Trie} Trie 高效地处理了前缀查询问题,能够在给定的约束条件下快速计算出平均击键次数。