1. 题目分析与解题思路
P6964 [NEERC 2016] Abbreviation这道题目考察的是字符串处理能力,要求我们识别特定格式的单词序列并将其转换为缩写形式。题目中的"word"定义非常严格:首字母必须大写,长度大于1,其余字母必须小写。而"word串"则是由多个这样的word组成,中间用单个空格分隔。
1.1 核心问题拆解
解决这个问题需要处理以下几个关键点:
- 单词识别:准确判断一个字符串是否符合题目定义的"word"标准
- word串检测:识别连续的word序列(中间用单个空格分隔)
- 转换逻辑:将符合条件的word串转换为缩写形式
- 非干扰内容处理:保持其他非word串内容不变
1.2 算法选择考量
这道题最适合使用**有限状态机(FSM)**的思路来解决,因为我们需要在不同的处理状态间切换:
- 普通字符处理状态
- word识别状态
- word串检测状态
- 缩写生成状态
使用状态机可以清晰地划分不同处理阶段,避免逻辑混乱。虽然也可以使用正则表达式,但对于初学者来说,状态机的实现方式更直观,也更容易调试。
2. 代码实现详解
2.1 辅助函数设计
首先我们实现两个核心辅助函数,用于判断字符是否大写和小写:
cpp复制bool isUpperCase(char x) {
return x >= 'A' && x <= 'Z';
}
bool isLowerCase(char x) {
return x >= 'a' && x <= 'z';
}
然后是判断一个字符串是否符合word定义的函数:
cpp复制bool isWord(const string &s) {
if (s.empty() || !isUpperCase(s[0]))
return false;
if (s.length() <= 1)
return false;
for (size_t i = 1; i < s.length(); ++i) {
if (!isLowerCase(s[i]))
return false;
}
return true;
}
注意:这里使用size_t而不是int来避免符号比较警告,这是C++中处理字符串长度时的最佳实践。
2.2 主处理逻辑
主处理逻辑分为以下几个步骤:
- 输入处理:逐行读取输入
- 分词:将每行内容拆分为单词和非单词部分
- word串检测:寻找连续的word序列
- 转换输出:对符合条件的word串进行缩写转换
cpp复制int main() {
string line;
while (getline(cin, line)) {
vector<string> tokens;
string current;
// 分词阶段
for (char c : line) {
if (isUpperCase(c) || isLowerCase(c)) {
current += c;
} else {
if (!current.empty()) {
tokens.push_back(current);
current.clear();
}
tokens.push_back(string(1, c));
}
}
if (!current.empty()) {
tokens.push_back(current);
}
// 缩写处理阶段
for (size_t i = 0; i < tokens.size(); ) {
if (isWord(tokens[i])) {
size_t j = i;
// 检查后续是否是word串
while (j + 2 < tokens.size() &&
tokens[j+1] == " " &&
isWord(tokens[j+2])) {
j += 2;
}
if (j > i) { // 找到word串
// 输出缩写
for (size_t k = i; k <= j; k += 2) {
cout << tokens[k][0];
}
cout << " (";
// 输出全称
for (size_t k = i; k < j; ++k) {
cout << tokens[k];
}
cout << tokens[j] << ")";
i = j + 1;
} else {
cout << tokens[i];
++i;
}
} else {
cout << tokens[i];
++i;
}
}
cout << endl;
}
return 0;
}
2.3 关键代码解析
分词阶段的核心是将输入字符串拆分为字母序列(可能是word)和非字母字符。这里使用简单的条件判断:
- 如果是字母,加入当前token
- 如果不是字母,结束当前token(如果有),并将非字母字符作为单独token
缩写处理阶段使用双指针技巧:
- i指向当前处理的token
- j向后探索可能的word串
- 当发现word串时,先输出首字母缩写,再输出完整形式
3. 边界情况与测试验证
3.1 常见边界情况
- 单个word:不应缩写,如"Abb"应原样输出
- 非字母字符:标点符号、数字等应原样保留
- 连续空格:题目保证word间只有一个空格,但其他位置可能有多个
- 大小写混合:如"ABc"不符合word定义
- 行首行尾:确保处理不会越界
3.2 测试用例设计
除了题目提供的样例,还应测试以下情况:
text复制输入:
Hello World! This Is A Test.
Multiple Spaces Here.
SingleWord
aBc Def Ghi
输出:
HW (Hello World)! This Is A Test.
Multiple Spaces Here.
SingleWord
aBc Def Ghi
提示:在本地测试时,可以使用文件重定向来简化测试过程。将测试用例保存为input.txt,然后运行程序时使用:
./program < input.txt
4. 性能优化与代码改进
4.1 时间复杂度分析
该算法的时间复杂度是O(n),其中n是输入的总字符数。因为:
- 每个字符只被处理一次(分词阶段)
- 每个token也只被处理一次(缩写阶段)
4.2 空间优化
当前实现使用了额外的vector存储tokens,空间复杂度是O(m),其中m是每行的token数。可以优化为流式处理,不保存所有token:
cpp复制// 流式处理伪代码
while (读取字符) {
if (当前处于word中) {
if (遇到非字母字符) {
判断是否满足word条件
尝试向后探测word串
输出相应内容
}
} else {
直接输出非字母字符
}
}
4.3 代码可读性改进
- 使用枚举定义处理状态,使逻辑更清晰:
cpp复制enum State {
NORMAL,
IN_WORD,
IN_SPACE
};
- 将缩写处理提取为独立函数:
cpp复制string generateAbbreviation(const vector<string> &words) {
string abbr;
for (const auto &word : words) {
abbr += word[0];
}
abbr += " (";
for (size_t i = 0; i < words.size(); ++i) {
if (i != 0) abbr += " ";
abbr += words[i];
}
abbr += ")";
return abbr;
}
5. 常见错误与调试技巧
5.1 典型错误列表
-
边界条件处理不当:
- 忘记检查单词长度>1
- 没有正确处理行尾情况
-
空格处理错误:
- 将多个空格视为分隔符
- 忘记非word间的空格也需要原样输出
-
大小写判断错误:
- 使用错误的ASCII值范围
- 忽略非字母字符的影响
5.2 调试建议
- 使用小测试用例:先确保简单情况正确,再处理复杂输入
- 打印中间结果:在分词后和缩写前打印tokens数组
- 单元测试辅助函数:单独测试isWord函数是否正确
- 内存检查:使用valgrind等工具检查内存泄漏
5.3 实际调试案例
假设遇到以下错误输出:
text复制输入:"Hello World"
输出:"H (Hello)W (World)"
问题分析:
- 没有正确识别连续的word串
- 将两个word分别处理而非作为一个word串
解决方案:
- 修改word串检测逻辑,确保连续word+空格+word被整体处理
- 添加调试输出,确认检测到的word串范围
6. 算法扩展与应用
6.1 类似题目练习
- 字符串替换:实现更通用的缩写替换功能
- 单词统计:统计特定格式单词的出现频率
- 文本格式化:按照特定规则重新排版文本
6.2 实际应用场景
- 文档处理:自动生成专业术语表
- 代码重构:识别并替换重复的代码片段
- 自然语言处理:文本标准化预处理
6.3 进阶挑战
- 支持更多缩写形式,如:
- "International Business Machines" → "IBM"或"I.B.M."
- 允许自定义缩写规则
- 处理更复杂的单词定义:
- 允许包含数字
- 支持连字符连接的单词
- 多语言支持:
- 非ASCII字符处理
- 不同语言的大小写规则
在解决这类字符串处理问题时,最重要的是先明确需求定义,设计清晰的算法流程,再逐步实现和测试。这道题目很好地训练了我们对字符串操作的精确控制能力,也展示了如何将复杂规则转化为可执行的代码逻辑。
