华为OD机试敏感字段加密:状态机与区间替换实战解析 华为OD机试的题库里字符串处理题占了很大比重“敏感字段加密”就是其中一道很有代表性的题目。我第一次做这道题时代码跑完样例输出和预期差了十万八千里——问题就出在连续下划线和引号转义这两个不起眼的规则上。后来把思路理顺才发现这题其实不难难的是在机试那种紧张状态下能不能把边界条件一次想全。这篇文章把我从读题到AC的完整过程写出来题目的隐藏规则是什么、为什么要用“下标区间替换”而不是split、Python/Java/C三种代码分别怎么写、以及我总结的8个自测用例。无论你是刚开始准备华为OD上机考试还是已经有基础想快速过一遍典型题都可以直接拿这篇做参考。1. 华为OD机试里的“敏感字段加密”到底考什么1.1 第一题里的“送分题”与“送命题”在华为OD机试的题目分布里敏感字段加密通常出现在靠前的位置分值不算高却非常能拉开差距。很多考生觉得它就是个“按下划线拆字符串、换第k个字段”的简单题结果提交之后发现通过率只有百分之三四十甚至直接0分。为什么因为这题表面上考的是字符串分割实际上考的是带引号、带转义、带连续分隔符的字段解析这些恰恰是平时刷题最容易忽略的角落。从考察能力来看它属于最典型的“状态机”题目你需要记住自己当前是不是在引号内部遇到反斜杠要不要跳过下一个字符遇到下划线要不要结束当前字段。只要这几个判断写对了整道题的核心就完成了。再加上一个越界判断和一个字符串拼接满分就是你的。所以我的建议是不要因为它简单就轻视也不要因为第一遍做错了就怀疑自己。把这题当成“字符串解析类”的代表作吃透后面遇到命令行参数解析、日志字段提取之类的问题你会非常受益。1.2 题目规则还原输入输出和六个约束先把题目还原一下。输入有两行第一行一个命令字符串由多个敏感字段组成字段之间用下划线_分隔。第二行一个整数 k表示要替换的字段索引从0开始。输出将第 k 个字段替换成六个星号******后的完整字符串如果 k 超出有效字段范围输出ERROR。规则里有几个点必须逐字确认规则说明分隔符字段之间以一个或多个下划线_分隔连续下划线只算一个分隔符引号双引号括起来的部分是一个完整字段引号内的下划线不参与分隔转义双引号内的\表示一个字面双引号不代表字段结束空字段字符串开头或结尾的下划线会产生空字段空字段也是有效字段替换字符是******恰好6个星号一个都不能少索引范围0 k 字段总数否则输出大写的ERROR这里最容易犯的两个错一是把连续下划线当成“产生了空字段”二是把转义引号当成字段结束。后面我会用样例逐个说明。2. 样例背后的隐藏规则为什么不能直接split2.1 用 password__a12345678_timeout_100 反推连续下划线语义看一个最常见的样例输入password__a12345678_timeout_100 1输出password__******_timeout_100如果你第一反应是用split(_)得到的结果是[password, , a12345678, timeout, 100]索引1是一个空字符串。把索引1替换成星号再拼回去得到的是password_******_a12345678_timeout_100下划线数量不对和样例对不上。如果你用split(_)正则把多个连续下划线当一个分隔符得到[password, a12345678, timeout, 100]替换索引1后拼回去是password_******_timeout_100仍然只有一个下划线还是对不上。为什么样例输出是两个下划线因为它要求“把第1个字段替换成星号”同时“除被替换字段外其余字符原样保留”。原字符串里password和a12345678之间本来就有两个下划线这两个下划线属于分隔符的一部分替换时不能把它们吞掉。所以结论很明确这题不能“分割后重新拼接”正确做法是“记录每个字段在原字符串中的起止位置然后只替换目标字段那个区间”。2.2 引号、转义和空字段的边界行为除了连续下划线引号和转义是第二个大坑。比如字符串ab_cd_ef如果不知道规则你会以为字段是ab、cd、ef三个。实际上第一个双引号开始了一个字段直到遇到第二个双引号才结束所以ab_cd整体是一个字段里面的下划线只是字段内容不是分隔符。替换时如果 k1应该把这个带引号字段整体换成星号输出ab_cd_******。再比如a\b_c字段内容是a\b其中\表示一个普通的双引号字符它不会提前结束字段。这里反斜杠的作用是“转义下一个字符”所以遍历时看到反斜杠必须把它和后面的字符一起跳过去。空字段也很微妙。字符串_abc在标准分隔语义下第一个字段是空字符串位于开头_之前第二个字段是abc字符串abc_则在末尾_之后有一个空字段。这种用例在机试中不一定出现但你实现的代码必须逻辑自洽不能因为“看着像多余的下划线”就破坏原有的字符结构。3. 核心解法一次遍历的状态机 区间替换3.1 先想清楚“替换后保留什么”再动手踩过 split 的坑之后我总结出的核心思路是解析时只记录边界不修改、不重组原字符串。具体来说每一步要记录的是每个字段在原字符串里的起始下标和结束下标。这样替换第 k 个字段时只需要做一次切片拼接把第 k 个字段之前的字符原样留下中间放六个星号再把第 k 个字段之后的字符原样接上。这种做法的好处是连续下划线保留下来了引号保留了转义反斜杠保留了一切没有被替换的字符都毫发无损。样例中两个下划线的问题自然就解决了。用一个比喻理解你手里有一排积木中间某一块要换成“星号”积木。正确做法是先把这块积木的位置记下来抽掉它把星号放进去其他积木一块不动。如果你先把整排积木拆散再一个个重新搭顺序和数量就很可能对不上原样。3.2 引号状态机怎么设计这一步是整个程序的灵魂。我习惯维护一个布尔变量inQuote表示当前是否在一对双引号内部。遍历字符串时在引号外遇到进入引号状态遇到_说明当前字段结束记录区间然后跳过所有连续的下划线其他字符正常往后移动。在引号内遇到\且后面还有字符说明是转义序列题目约束下主要是\跳过下一个字符遇到退出引号状态其他字符正常往后移动。用伪代码写出来大概是i 0 start 0 inQuote False while i n: if inQuote: if s[i] \\ and i 1 n: i 2 continue if s[i] : inQuote False i 1 else: if s[i] : inQuote True i 1 elif s[i] _: fields.append((start, i)) while i n and s[i] _: i 1 if i n: fields.append((n, n)) return fields start i else: i 1 if start n: fields.append((start, n))这里最需要注意的是在引号内遇到反斜杠时不是“把反斜杠扔掉”而是“跳过下一个字符”。因为\是一个整体引号不应该被当作字段结束符去处理。3.3 从字段区间到最终输出解析得到的fields是一个个(start, end)区间。例如字符串ab_cd_ef解析结果是字段索引startend字段内容007ab_cd1810ef替换第 k 个字段就是result s[0:fields[k].start] ****** s[fields[k].end:n]如果k不满足0 k len(fields)直接输出ERROR。这样写非常直观不容易出错也方便调试。你可以随时打印fields来验证自己的解析逻辑是否正确。4. Python实现最短代码背后的两个输入坑4.1 可直接复制的Python解法Python 写这类题最顺手代码量也最少。下面是我在牛客/OD机试环境中验证过的版本import sys def parse_fields(s): n len(s) fields [] i 0 start 0 in_quote False while i n: if in_quote: # 引号内的转义\ 是一个整体跳过下一个字符 if s[i] \\ and i 1 n: i 2 continue if s[i] : in_quote False i 1 else: if s[i] : in_quote True i 1 elif s[i] _: fields.append((start, i)) # 跳过连续下划线 while i n and s[i] _: i 1 if i n: fields.append((n, n)) return fields start i else: i 1 if start n: fields.append((start, n)) return fields def main(): lines sys.stdin.read().splitlines() if len(lines) 2: return s lines[0].strip() k int(lines[1].strip()) fields parse_fields(s) if k 0 or k len(fields): print(ERROR) else: l, r fields[k] print(s[:l] ****** s[r:]) if __name__ __main__: main()这个版本可以直接提交核心逻辑都在parse_fields里。4.2 反斜杠字面量、空字段和多行输入的坑写 Python 时有几个细节值得单独提醒。第一代码里的\\表示的是一个反斜杠字符。很多人会写成/或者写两个反斜杠但搞混含义。Python 字符串中\\才是长度为1的反斜杠字符。第二sys.stdin.read().splitlines()比连续用两次input()更稳。它能把第一行可能出现的空字符串也正确读出来比如第一行本身是空串时splitlines()会返回[, 1]这样lines[0]就是空串程序会得到空字段列表并输出 ERROR逻辑上不会崩。如果你用input()第一行为空串时读到的也是空串其实也没问题但万一输入末尾有多余空行read().splitlines()处理起来更干净。第三不要对输入字符串做任何额外的替换或解码。比如不要用s.replace(\\, )去“还原”转义序列因为输出时要保留原始转义字符。你解析时知道\是一个整体就够了不要改动原串。第四fields可能是空列表。比如第一行是空字符串或者输入只有一行时k无法读取这种情况下k必然越界输出ERROR是正确行为。5. Java实现不要用split用substring保留原串5.1 完整的Java实现Java 写起来比 Python 繁琐一点但思路完全一致。注意 Java 的String是不可变对象所以“替换区间”需要通过substring拼接完成。import java.util.ArrayList; import java.util.List; import java.util.Scanner; public class Main { static class Range { int start; int end; Range(int start, int end) { this.start start; this.end end; } } static ListRange parseFields(String s) { ListRange fields new ArrayList(); int n s.length(); int i 0; int start 0; boolean inQuote false; while (i n) { char c s.charAt(i); if (inQuote) { if (c \\ i 1 n) { i 2; continue; } if (c ) { inQuote false; } i; } else { if (c ) { inQuote true; i; } else if (c _) { fields.add(new Range(start, i)); while (i n s.charAt(i) _) { i; } if (i n) { fields.add(new Range(n, n)); return fields; } start i; } else { i; } } } if (start n) { fields.add(new Range(start, n)); } return fields; } public static void main(String[] args) { Scanner sc new Scanner(System.in); String s sc.hasNextLine() ? sc.nextLine() : ; int k sc.hasNextInt() ? sc.nextInt() : -1; ListRange fields parseFields(s); if (k 0 || k fields.size()) { System.out.println(ERROR); } else { Range range fields.get(k); String result s.substring(0, range.start) ****** s.substring(range.end); System.out.println(result); } } }这个类名是Main符合大部分 OJ 平台的提交要求。如果你用的平台不是Main记得改类名。5.2 Java里String不可变带来的编写习惯Java 实现有几点值得说明。首先是为什么不用split。String.split(_)遇到了连续下划线会给空字段而split(_)会把连续下划线当成一个分隔符但无论哪种重新拼接时都会破坏原始字符串的字符结构。这道题的正确姿势就是用substring做切片substring(0, range.start)和substring(range.end)分别取出目标字段前后的内容。其次是charAt的边界问题。当i 1 n不存在时不要访问s.charAt(i 1)。代码里已经用条件i 1 n做了保护防止在字符串末尾访问越界。再次是Scanner的读取顺序。这里一定是先nextLine()读第一行字符串再用nextInt()读第二行的k。如果反过来先nextInt()再nextLine()nextLine()会读到数字后面的换行符导致字符串变成空串。这种坑在 Java 里太常见了。最后比较字符串时不要用要用equals。不过这道题里不需要比较字段内容只做区间替换所以只要保证substring的边界正确就行。6. C实现下标遍历时要管住边界6.1 完整的C实现C 和 Java 的流程几乎一样区别在于 C 的string可以直接用下标访问拼接时用substr。#include iostream #include string #include vector using namespace std; struct Range { int start; int end; Range(int s, int e) : start(s), end(e) {} }; vectorRange parseFields(const string s) { vectorRange fields; int n s.size(); int i 0; int start 0; bool inQuote false; while (i n) { char c s[i]; if (inQuote) { if (c \\ i 1 n) { i 2; continue; } if (c ) { inQuote false; } i; } else { if (c ) { inQuote true; i; } else if (c _) { fields.emplace_back(start, i); while (i n s[i] _) { i; } if (i n) { fields.emplace_back(n, n); return fields; } start i; } else { i; } } } if (start n) { fields.emplace_back(start, n); } return fields; } int main() { string s; getline(cin, s); int k; cin k; vectorRange fields parseFields(s); if (k 0 || k (int)fields.size()) { cout ERROR endl; } else { const Range r fields[k]; cout s.substr(0, r.start) ****** s.substr(r.end) endl; } return 0; }这段代码我在本地测试过也模拟过 OJ 的输入方式可以直接用。6.2 C特有的三个注意点C 的坑比 Python 和 Java 更偏向“底层细节”我踩过三次简单罗列一下。第一输入读取顺序。这里用getline(cin, s)读第一行再用cin k读第二行。这个顺序是安全的。如果反过来先cin k再getline第二行会读到一个空字符串因为cin k不会吞掉行尾的换行符。很多初学者在这个地方莫名其妙读不到字符串。第二下标访问的边界。在引号内遇到反斜杠时i 1 n这个判断非常重要。C 的string访问越界是未定义行为不会像 Java 那样给你抛异常而是可能随机读到脏数据导致线上行为和本地不一样。所以反斜杠在字符串末尾这种异常输入必须保护住。第三fields.size()返回的是size_t本质上是无符号整数。如果你直接拿k和它比较当k是负数时k会被转换成很大的无符号数导致负数反而通过判断。所以要么先判断k 0要么把fields.size()转成int再比较。代码里两种方式都用上了属于双保险。另外emplace_back(start, i)是 C11 的写法如果你用的编译器比较老可以改成fields.push_back(Range(start, i))。现在 OJ 基本都支持 C11问题不大。7. 提交前必测的8个用例把隐藏case一次打穿7.1 边界用例表代码写完后我通常不会直接交而是先过一遍自测用例。下面这几个是我总结出来必测的你可以直接复制到本地跑输入第一行k期望输出考察点password__a12345678_timeout_1001password__******_timeout_100连续下划线保留abc_def_ghi1abc_******_ghi普通带引号字段ab_cd_ef1ab_cd_******引号内下划线不算分隔符a\b_c1a\b_******转义双引号不结束字段_abc0******_abc前导下划线产生空字段abc_1abc_******尾部下划线产生空字段abc_abc2ERROR索引越界空字符串0ERROR无字段输入前四个用例是每个 OD 题解基本都会提到的后四个是我额外加的边界用例用于确认代码在各种输出下不会崩溃。7.2 每个用例背后的考察点password__a12345678_timeout_100是最核心的用例它同时验证了“连续下划线只算一个分隔符”和“替换时保留原分隔符”这两条规则。如果你的代码在第一个用例就错了后面都不用看方向一定偏了。abc_def_ghi和ab_cd_ef验证引号状态机。注意ab_cd里有一个下划线它出现在引号内部所以字段不会被切开。如果这里解析成三个或更多字段代码对引号的处理就有问题。a\b_c验证转义。这个用例有个隐蔽点字符串里的\是两个字符解析时应该一起跳过不能让里面的把引号状态关掉。很多第一次写这道题的人就是栽在这里。_abc和abc_考的是空字段。我的实现按照标准字符串分割语义把前导下划线之前和尾部下划线之后视为空字段。如果你的环境下题目明确规定“空字段无效”可以在这两个用例上调整解析逻辑但整体状态机框架不变。abc_abc和空字符串考的是越界处理。注意题目要求输出的ERROR是大写不能写成Error或者errorOJ 对输出是严格比对的。建议在本地把这8个用例全部跑一遍再提交。跑完如果全过这题的分数基本稳了。8. 考场上的时间分配与同类题迁移8.1 十分钟稳拿这题的答题节奏我的习惯是把这类题控制在十分钟以内节奏大概是这样的先用两分钟读题、看样例。重点不是急着理解“什么是敏感字段”而是从样例反推规则连续下划线怎么处理、引号怎么处理、越界输出什么。样例就是题目给出的最好提示。再用五分钟写核心状态机。我一般先写一个空的while循环把inQuote变量写上再逐步补成分引号内判断转义、引号外判断下划线。语法可以慢但逻辑框架不能乱。最后留三分钟跑自测用例。把上面8个用例在本地过一遍确认输出没有差异后再提交。这一步尤其关键因为很多错误不是逻辑没想清楚而是下标越界、负数比较、转义判断少了一个条件之类的小毛病。如果你在考场上时间紧张至少也要跑完前四个用例尤其是连续下划线和转义引号。8.2 从“敏感字段加密”迁移到其他字符串解析题这道题的价值不只在华为OD。它的核心思想是“带状态地遍历字符串记录边界再操作区间”。这个套路可以迁移到很多场景命令行参数解析参数之间用空格分隔带引号的参数可能包含空格还要处理转义CSV文件解析逗号分隔双引号内的逗号不是分隔符双引号本身转义日志字段提取按固定分隔符切字段但某些字段被引号包裹内部可能有分隔符表达式解析括号配对、引号配对本质上也是状态机。遇到这类问题先问三个问题分隔符是什么哪些情况下分隔符失效转义规则是什么把这三个问题回答清楚状态机的设计基本就出来了。我个人在机试中体会到字符串题最怕的不是想不到而是想得太快。很多考生看完样例直接split一把梭结果漏掉边界条件。慢一点把自己当成解析器去模拟一遍输入比多写十行代码更有用。最后分享一个小技巧调试这类题时可以在解析完成后先打印fields列表看看字段数量和区间对不对。如果fields和预期一致替换只是切片的事情如果fields都不对问题一定出在状态机的几个分支上。先定位解析再处理输出排查效率会高很多。