OJ“检查密码”题全解析:规则拆解与边界条件避坑
发布时间:2026/10/20 19:08:17 锦皓数字建站

“检查密码”这四个字我在某高校OJ平台的实验九里见到时第一反应是“这题不会太难吧”。结果你猜怎么着同一批作业里它排在26题看着就是给新手练字符串处理的可身边不少同学交了七八次还在WA。后来我把题面规则、判题环境、代码实现从头到尾复盘了一遍才发现这类题真正的考点根本不是“会不会写if”而是规则拆得够不够细、边界想得够不够全、字符处理有没有踩到平台本身的坑。这篇文章我尽量把东西写透题面规则怎么还原、五个判定条件怎么拆开验证、判题系统里那些看不见的边界以及C、Python、Java三版参考实现和一套提交前的测试清单。适合刚学完数组和字符串、准备第一次和别人在OJ上拼通过率的人读也适合那些想从“能过题”进阶到“代码写干净”的同学参考。1. 实验九最后一道题的题面还原与规则边界1.1 原始题面很模糊我先按最常用的版本补充规则到手的原始信息其实只有一个标题检查密码。真正做过这类题的人都知道出题人通常不会好心把所有细节写在标题里完整的规则都在题面正文中而且每个学校每年的题面可能都不一样。为了把分析讲明白我按自己在同类OJ题里见到概率最高的规则来还原密码长度必须在 6 到 16 位之间包含边界本身密码必须同时包含大写字母、小写字母、数字密码必须至少包含一个特殊字符这里的“特殊字符”按可见标点符号处理密码中不能出现连续三个相同的字符。如果你拿到的题面规则跟我写的不完全一样比如要求长度 8 到 20或者只要满足四类字符中的三类又或者不检查连续重复——不要慌下面每一段拆开的判定单元是可以直接替换的。真正值钱的不是某一条规则而是拆规则和验证规则的方法。1.2 这类题真正隐藏的判定点不在“长度”而在“字符”很多第一次写的人会以为检查密码就是if (strlen(s) 6 strlen(s) 16)然后再数一下里面有没有数字。一旦你这么想WA就跑不掉了。我看过最典型的错法是只判断了长度和数字忘了大写、小写、特殊字符各算一类还有的人把“连续三个相同字符”理解成“相邻两个相同”直接漏掉 1111 这种四连重复的用例。还有一类隐蔽问题是字符范围。C语言的char可能是带符号的直接拿s[i]去调用isupper、isdigit这类函数时如果字符不在 ASCII 可见范围内行为是未定义的。这一条在OJ上不一定会触发但一旦测点里出现中文、扩展字符或者\xff这类值你可能连“为什么本地好好的提交就WA”都找不到原因。老规矩先别碰代码把这道题的要求画成一张判定表判定条件合法密码必须满足不满足时的行为长度6 len 16直接判为非法不用继续看字符大写字母至少一个直接判为非法小写字母至少一个直接判为非法数字至少一个直接判为非法特殊字符至少一个直接判为非法连续重复不能连续出现3次同字符直接判为非法这张表看起来平平无奇但把它写下来之后你的代码结构就变成“先长度、后字符、再重复”的三段式。后面不管规则怎么变你只需要改表里的一行而不需要把整个程序推倒重来。2. 把“检查密码”拆成五个可以独立验证的判定单元2.1 先定数据读取别让换行符背黑锅OJ题目的输入通常是一组或者多组测试数据。常见写法是先读一个整数T表示后面有T行密码然后每行读一个字符串。这里的第一坑就在“每行”这两个字。如果你用scanf(%d, T)读完数字紧接着用fgets读密码第一次fgets会先把数字后面那个换行符读掉于是你得到的是一个空行真正密码被当成第二次输入。这不是密码检查的问题而是混合读取行缓冲和fgets时的经典事故。更稳妥的做法是读完T之后立刻getchar()把残留的换行符消费掉或者干脆第一行也用fgets后再sscanf从源头上避开缓冲区状态不一致。我自己的习惯是判断题主数据规模不大用scanf读T加一个getchar()就够但心里始终记着这一行换行符的存在。2.2 字符分类最好交给库函数不要手搓 ASCII 区间判断一个字符是不是大写字母新手最喜欢写if (ch A ch Z)。这没错可一旦规则扩展到“特殊字符”“空白字符”“数字字符”手写区间就容易漏。比如特殊字符如果你写ch ! ch ~看起来覆盖了 ASCII 标点但空格也被包含进来了如果你用排除法!(ch a ch z) !(ch A ch Z) !(ch 0 ch 9)那空格同样会被误判成“特殊字符”。到底空格算不算特殊字符这要看题面。但如果规则说的是“标点符号”那空格显然不属于这一类。C语言的ctype.h提供了现成的分类函数isupper、islower、isdigit、ispunct。使用它们的额外好处是可读性强将来改成判断中文字符或 Unicode 代码点时思路可以平移。需要注意的是调用前把char转成unsigned char这是C语言老手都会主动做的一步。同样的思路在 Python 里对应str.isupper()、str.islower()、str.isdigit()、str.isalnum()在 Java 里对应Character.isUpperCase()、Character.isLowerCase()、Character.isDigit()、Character.isLetterOrDigit()。库函数的好处是你不用记住每个字符的具体编码也不用担心平台字符集差异。2.3 连续重复字符只看当前和前面两位不需要额外状态“不能连续出现三个相同字符”的判定最不需要写成状态机。一个循环从下标2开始每次看s[i]、s[i-1]、s[i-2]是否全部相等。一旦相等立刻返回不合法。这里很多人会犯的错是只比较s[i]和s[i-1]把规则记成了“相邻两个不能相同”。如果你用aaa来做测试两种写法都拦得住但遇到aaaa只比较相邻的代码会漏掉吗不会它一样拦得住。真正漏的是aaabaaa也不是只要遇到aaa局部就会触发。那为什么说只比较相邻不对因为你把条件写弱了比如题目要求“不能出现连续三个相同”意思其实是aaa非法aa合法而aaaa里面也包含aaa所以非法。只检查相邻两个是否相等会让aaaa被放过去吗不会。这其实是概念不清晰而不是实际WA但在读代码和向别人解释时会非常别扭。从工程上讲把“三连重复”写成独立函数还有另一层好处将来题目改成“不能出现连续四个相同”你只需要把比较往前推进一位其他逻辑不用动。代码如下int has_triple_repeat(const char *s) { int len (int)strlen(s); for (int i 2; i len; i) { if (s[i] s[i - 1] s[i] s[i - 2]) { return 1; } } return 0; }2.4 用短路逻辑把五个判定串起来五个判定条件最好独立成函数然后在主逻辑里用短路方式组合先判长度长度不合法就直接输出NO不要再继续访问字符防止后面代码对空字符串或者超长字符串做多余操作。这里还有个细节如果密码长度超过题目限制比如 100 个字符你仍然遍历全串判断字符类型虽然不会出错但白白浪费了时间。把长度判断放在最前面既符合逻辑又能在超长测点上省掉一次扫描。代码结构大概是这样int check_password(const char *s) { int len (int)strlen(s); if (len 6 || len 16) { return 0; } if (!has_upper(s) || !has_lower(s) || !has_digit(s) || !has_special(s)) { return 0; } if (has_triple_repeat(s)) { return 0; } return 1; }3. 判题环境最容易翻车的四个细节与排查链路3.1 翻车现场一T组输入时字符串读取被上次残留干扰这个问题的表现是本地测试第一组数据正常第二组开始第一个字符莫名消失或者整个字符串变成空行。原因几乎都是scanf和fgets混用导致缓冲区里还有换行符。我当时调试时在每组读入后马上打印printf([%s]\n, s);然后发现第二组输出变成[]才知道是换行符被读成一整行了。解决方式很简单读完整数后用getchar()清掉换行或者统一改用fgets读每一行再用sscanf解析。为了通用我更建议写一个干净的读取函数把“去换行”和“修剪\r”都封装进去。3.2 翻车现场二fgets 把换行符当成密码内容用fgets读入字符串后字符串末尾会带着一个\n。你没去掉它时长度永远比你看到的字符数多 1。比如密码Abc123!明明只有 7 个字符你却得到 len 8直接把长度判断卡在 6 到 16 的临界点附近疯狂出错。修法是用strcspn找到第一个换行符的位置并替换成\0s[strcspn(s, \n)] \0;Windows 环境有时还会混入\r所以更稳一点的做法是连续把\r和\n都清掉。某类题目如果明确说密码“不含空格”也有人直接改用scanf(%s, s)但遇到字符串里可能包含空格的情况就必须回到fgets因此我建议直接养成“读取后清理换行”的肌肉记忆。3.3 翻车现场三把特殊字符定义成“不是字母数字的一切”我第一次写has_special时用的逻辑是“遍历所有字符只要不是大小写字母和数字就算特殊字符”。这个写法在遇上空格时会立刻判断成功。可是密码规则里如果写的是“必须包含特殊符号”空格一般不应当被当成合法特殊字符。这条坑很隐蔽因为本地测试时你可能专门挑了Abc123$这类带$的样例完全没测带空格的输入。等你哪天手滑输入Abc 123程序会直接判YES跟预想不一致。最终我把has_special改成用ispunct空格被排除在外它既不是字母也不是数字也不是标点所以不会触发“特殊字符已出现”的标志。真正的教训是字符分类不能光用排除法最好同时考虑“它是什么”和“它不是什么”。字母、数字、标点、空白是四类不同概念混在一起写迟早出事。3.4 翻车现场四带符号 char 传给 ctype 函数这个问题不那么容易遇到但遇到了就是玄学。C标准里isupper这类函数要求参数能表示为unsigned char或EOF否则是未定义行为。如果机器上char默认带符号那么s[i]的高位为 1 时传给函数会变成负数查表直接越界结果完全不可预期。修法是每次调用前先转一次isupper((unsigned char)s[i])。虽然这一行看起来啰嗦但在跨平台提交时能帮你省掉一批“本地正常、OJ WA”的深夜崩溃。3.5 一套可以反复用的调试图标法遇到WA我习惯在代码里临时保留一组“裁判输出”在每个判定函数返回值后面打印一个标志位比如长度 01、大写 02、小写 03、数字 04、特殊 05、重复 06然后输入一组故意构造的密码观察哪个标志位不符合预期。这比一步一步断点更快尤其是OJ题没法逐步调试时。不要觉得打印代码很土。OJ题真正要保的是最终正确性不是代码整洁度。等确认全过之后再把这些调试输出删掉重交一次干净又安全。4. 参考实现C、Python、Java 三版对照4.1 C 版本最贴近课程OJ的标本答案这门课既然叫“实验九”大部分同学用的应该是 C。C 版本放到OJ上的好处是编译快、运行快、语法限制少而且课程的样例评测一般也是用 GCC 跑。下面这份代码可以直接复制进编辑器里验证但比赛提交前请删除调试打印。#include stdio.h #include string.h #include ctype.h int has_upper(const char *s) { for (int i 0; s[i]; i) { if (isupper((unsigned char)s[i])) return 1; } return 0; } int has_lower(const char *s) { for (int i 0; s[i]; i) { if (islower((unsigned char)s[i])) return 1; } return 0; } int has_digit(const char *s) { for (int i 0; s[i]; i) { if (isdigit((unsigned char)s[i])) return 1; } return 0; } int has_special(const char *s) { for (int i 0; s[i]; i) { if (ispunct((unsigned char)s[i])) return 1; } return 0; } int has_triple_repeat(const char *s) { int len (int)strlen(s); for (int i 2; i len; i) { if (s[i] s[i - 1] s[i] s[i - 2]) { return 1; } } return 0; } int check_password(const char *s) { int len (int)strlen(s); if (len 6 || len 16) return 0; if (!has_upper(s) || !has_lower(s) || !has_digit(s) || !has_special(s)) return 0; if (has_triple_repeat(s)) return 0; return 1; } int main(void) { int T; scanf(%d, T); getchar(); while (T--) { char s[1024]; fgets(s, sizeof(s), stdin); s[strcspn(s, \n)] \0; printf(%s\n, check_password(s) ? YES : NO); } return 0; }这里缓冲区长度没有写太死用了一个相对宽裕的 1024。实际OJ通常会限定每行不超过 50 或 100给几倍余量总是对的。如果你确定字符串长度不超过 16也可以直接给 32但多留空间不是坏习惯。4.2 Python 版本能用库函数就用库函数别硬写正则Python 写这种题非常快几个内置方法就能完成字符分类。要注意的点是str.isalnum()对中文也会返回 True所以如果平台测点包含中文字符你要先想清楚中文字符算不算字母或特殊字符。def has_triple_repeat(s: str) - bool: for i in range(2, len(s)): if s[i] s[i - 1] s[i - 2]: return True return False def check_password(s: str) - bool: if not (6 len(s) 16): return False has_upper any(c.isupper() for c in s) has_lower any(c.islower() for c in s) has_digit any(c.isdigit() for c in s) has_special any((not c.isalnum()) and (not c.isspace()) for c in s) if not (has_upper and has_lower and has_digit and has_special): return False if has_triple_repeat(s): return False return True你可能会想用正则re.fullmatch(r..., s)一行搞定但正则的优点是直观缺点是可读性差、扩展性差。如果题目只判断一次还好一旦你需要在多组数据里复用逐字符的语义函数反而更容易维护。4.3 Java 版本不要用 matches 一把梭Java 版本的思路和C完全一致但要注意String.matches默认对整个字符串做匹配正则稍微写错一个边界就会出事。我建议老老实实逐字符遍历依赖Character来判断类型。public class Main { static boolean hasTripleRepeat(String s) { for (int i 2; i s.length(); i) { if (s.charAt(i) s.charAt(i - 1) s.charAt(i) s.charAt(i - 2)) { return true; } } return false; } static boolean checkPassword(String s) { if (s.length() 6 || s.length() 16) return false; boolean upper false, lower false, digit false, special false; for (int i 0; i s.length(); i) { char c s.charAt(i); if (Character.isUpperCase(c)) upper true; else if (Character.isLowerCase(c)) lower true; else if (Character.isDigit(c)) digit true; else if (!Character.isLetterOrDigit(c) !Character.isWhitespace(c)) special true; if (i 2 c s.charAt(i - 1) c s.charAt(i - 2)) return false; } return upper lower digit special; } public static void main(String[] args) { System.out.println(checkPassword(Abc123!) ? YES : NO); } }Java 这里有个天然好处char是十六位无符号值不会像C那样遇到带符号char的未定义行为。但charAt处理的是 UTF-16 编码单元如果测点出现某些增补字符判断逻辑需要换成codePointAt不过课程OJ出现这种数据的概率极低。4.4 三版对照下来我的选型理由用 C 当主力是因为这门课和 OJ 的评测环境默认支持 C提交简单运行快而且数组、指针、字符串的关系正好在这道题里全练到。Python 适合拿来本地造测试用例、快速验证规则边界因为脚本语言写一小段正则和循环都很快。Java 则适合练习类封装和Character方法不过对新生而言C 版仍是投入产出比最高的选择。语言主要优势主要风险推荐场景C贴近系统底层OJ兼容最好unsigned char转换、buffer清理课程实验、严苛运行环境Python代码量少字符串方法丰富isalnum对中文判定与题意可能不符快速原型、本地验证Java类型安全字符方法可靠代码偏长需要完整类结构练习工程化写法、面试手撕5. 提交之前自己先当一遍判题机5.1 十六个必测用例直接抄有些人觉得自己写的代码逻辑没问题就直接交然后被OJ反手一个WA教做人。我自己吃过太多次亏现在养成的习惯是先把测试用例写在纸上再跑代码。下面这一组用例覆盖了长度边界、字符缺失、连续重复、多组输入四种情况。输入: 8 abc12 Abc123 Abc123! Abc123!!a AAAAAA! AABBCC! Abc 123! Abc123!abc12长度 5不够6输出NO。Abc123长度 6有大写、小写、数字但没有特殊字符输出NO。Abc123!各项都满足输出YES。Abc123!!a长度超过16的一部分其实是9位满足条件输出YES。AAAAAA!有连续重复字符输出NO。AABBCC!虽然不重复三连但是否有大写、数字、特殊字符这里既有大写A、小写B、数字没有、特殊字符!所以缺数字输出NO。Abc 123!包含空格看特殊字符定义。按我的规则空格不算特殊但字符串里有!所以依然YES。Abc123!重复确认YES。这种表格别嫌琐碎每一行都对应一个判定分支。你手动把预期结果写下来再用程序跑一遍等于提前把OJ的测试点模拟了一部分。5.2 长度区间必须测“边边的邻居”长度规则是 6 到 16那测试时至少要看四组最短合法6位、最短非法5位、最长合法16位、最长非法17位。很多漏判都出在对界限的理解上有人把写成结果 6 位密码被算非法或者 17 位密码被放过去。更好的测试方式是写一个生成函数用循环构造出 5、6、16、17 位的字符串然后手动确认每一组输出。字符串不要只用一个字母凑长度比如AAAAAA确实长度为 6但它同时触发了连续重复规则你会分不清到底是因为长度还是因为重复才输出NO。正确的做法是让字符串既满足字符要求又卡在长度边上比如Ab1!Ab正好 6 位Ab1!Abcdefghijklmn正好 16 位。5.3 屏蔽“我写的代码一定没问题”的盲区交题之前最容易犯的错是只看输出 YES 还是 NO却不看判定顺序。建议临时加一行打印把每个子判断结果都带出来比如printf(len%d upper%d lower%d digit%d special%d triple%d\n, len, has_upper(s), has_lower(s), has_digit(s), has_special(s), has_triple_repeat(s));然后对每个样例观察一下输出尤其盯着特殊字符那一列。如果Ab1!Ab显示 special0说明你的特殊字符定义和预期不一致如果AAAAAA!显示 triple0说明重复判断根本没进循环。这种调试方法比对着屏幕空想快得多也更接近“判题机”的视角。5.4 多组数据时别让上一次的输入影响下一次判断多组数据场景下最常见的错误是某个标志变量没有重置。比如你在循环外定义了has_upper 0第一组数据读到大写字母后把它置1第二组数据没有大写字母但变量仍然是1于是误判YES。解决方法是把标志变量声明在每次循环内部或者每次循环开始前统一清零。6. 从“能过题”到“能复用”把检查逻辑朝工程化扩展一点6.1 把 YES/NO 改成错误列表这道题如果只输出 YES 或 NO实在有点浪费。实际项目里用户输入弱密码时你几乎永远要告诉他“到底哪里不够好”。稍微扩展一下写一个collect_errors函数把不满足的规则一条条加进链表或者字符串数组输出效果就完全不同。比如错误信息可以这样组织“密码长度应在 6 到 16 位之间”“需要包含大写字母”“需要包含小写字母”“需要包含数字”“需要包含特殊符号”“不能包含连续三个相同字符”然后主程序只做一件事先收集错误有错误就输出错误列表没错误就输出YES。这样代码的可读性、可测试性都上了一个台阶而且你在面试手撕代码时这种拆法也会让面试官更容易跟上你的思路。6.2 给密码强度打分还有一种常见扩展是强度评级长度、大小写、数字、特殊字符各占权重最后算出一个总分再映射成“弱、中、强”。实现起来并不难关键是权重怎么定。实际产品里长密码通常比强行堆符号更重要所以长度权重可以给高一点。课程实验不强制做这个但如果你想练一练模块化设计把这道题拆成“规则校验”和“强度评估”两个模块就是一次很好的小重构。6.3 这道题复盘完我最后留下三个习惯第一个习惯任何字符串题拿到手先问清楚字符范围。是ASCII还是UTF-8允不允许空格允不允许中文这些信息比代码本身更影响正确性。第二个习惯所有判定条件都做成独立函数。别在main里堆五个for独立函数让你可以单独测试每一个规则出了问题也能立刻定位。第三个习惯提交前必须跑一组带边界的用例而不是只跑题面样例。题面样例是出题人用来安抚你的真正的血泪都在边界测试里。我现在再去写类似题目时基本不会长篇大论调试了而是直接在纸上画出判定表按表格翻译代码然后跑一遍边界用例最后提交。整个过程大概十分钟但通过率比从前瞎试高得多。如果你手头正好也在写“检查密码”这道实验题不妨试试这个流程——先不要急着写代码把规则和边界用例列清楚你会发现自己离AC并不远。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。