PTA 作业集 4~6 总结:从基础逻辑门到子电路展开的迭代复盘

一、前言
本阶段的三次 PTA 作业集围绕“数字逻辑电路模拟”逐步展开,整体呈现出明显的迭代式特征:作业集 4 主要解决基础逻辑门与简单级联电路的计算问题,作业集 5 在已有逻辑门基础上扩展了更多组合逻辑元件,作业集 6 则进一步引入子电路、连接合法性检查和异常输出要求。三次作业不是孤立的三道题,而是同一类问题从“能算出结果”到“能表达复杂结构”,再到“能处理错误输入和模块复用”的连续训练。
从知识点看,本轮作业涉及 Java 面向对象设计、继承与多态、抽象类与接口、集合类、字符串解析、正则表达式、图结构建模、拓扑关系处理、迭代传播计算、异常情况判断以及输出顺序控制。和前几次偏基础语法的题目相比,本轮作业更强调对问题模型的抽象能力。单纯用顺序结构和若干 if-else 也可以完成一部分测试点,但当题目从单个逻辑门扩展到多层级联、复杂连接、子电路嵌套时,如果没有稳定的数据结构支撑,后续修改会非常被动。
从题量和难度看,作业集 4 的测试点共 40 个,我的提交结果为 95/100,最后一个“多层级联电路”测试点出现“非零返回”;作业集 5 的测试点共 34 个,最终 100/100;作业集 6 的测试点共 43 个,最终 93/100,其中 case8、case24、case29 出现答案错误。三次作业的难度是逐步上升的:第一次主要难在输入解析和逻辑值传播,第二次主要难在统一不同元件的管脚语义,第三次主要难在子电路展开、连接角色判断和异常优先级。
作业集 主要内容 测试点情况 得分
作业集 4 基础门元件、门级联组合、复杂连接、多层级联 40 个测试点,39 个通过 95/100
作业集 5 三态门、译码器、数据选择器、数据分配器等扩展元件 34 个测试点,全部通过 100/100
作业集 6 子电路、端口映射、异常检查、复杂连接展开 43 个测试点,40 个通过 93/100

总体而言,这三次作业让我明显体会到:程序规模一旦超过几百行,“能跑”并不等于“可靠”。如果前期的数据模型设计不清晰,后期每增加一个元件、一个异常规则或一种连接形式,都会使主函数越来越臃肿,错误也会更难定位。
二、设计与分析
2.1 源码规模与复杂度统计
我根据桌面上的源码和“源码和报错.docx”中的三次提交内容,对三版代码做了类似 SourceMonitor 的基础统计。统计项包括有效代码行、字符数、类或接口数量、方法数量以及分支关键字数量。这里的分支关键字数量不是严格的圈复杂度,但能够反映 if、for、while、switch、catch、逻辑与或等控制分支的增长趋势。
版本 有效代码行 字符数 类/接口/枚举数量 方法数量 分支关键字数量 主要结果
作业集 4 第一版 433 11602 9 66 73 95/100
作业集 5 第二版 479 13756 12 54 162 100/100
作业集 6 第三版 641 19003 11 50 199 93/100
桌面最终 Main.java 742 物理行,641 有效行 25610 11 50 199 本地 javac 通过

下表为通过本机 C:/Program Files (x86)/SourceMonitor/SourceMonitor.exe 直接生成的 CSV 报表内容摘录,未再使用自制图片代替 SourceMonitor 报表。原始文件保存在 C:/Users/jack/Desktop/pta-blog-4-6-professional/pta4、pta5、pta6 目录下,包括 -details.csv、-details.xml 和 .smproj 项目文件。
Project Name Checkpoint Name File Name Lines Statements Percent Branch Statements Method Call Statements Percent Lines with Comments Classes and Interfaces Methods per Class Average Statements per Method Name of Most Complex Method* Maximum Complexity* Maximum Block Depth Average Block Depth Average Complexity*
pta4 PTA4 Main.java 432 322 21.1 144 0.0 9 7.44 2.24 Main.main() 30 8 2.19 4.92
pta5 PTA5 Main.java 477 502 30.3 154 0.0 12 5.00 6.93 Main.main() 71 9+ 2.47 3.68
pta6 PTA6 Main.java 641 630 29.2 304 0.2 11 4.73 10.40 Main.main() 47 7 2.85 5.00

以下三张图为在本机 SourceMonitor 中打开对应 .smproj 项目后,进入 Metrics Details For File 'Main.java' 窗口所得的原生软件截图。截图中既包含每个文件的 Lines、Statements、Percent Branch Statements、Classes and Interfaces、Average Statements per Method 等统计项,也包含 Kiviat Graph 和 Block Histogram,可以更直观地观察三次作业中代码规模和块深度分布的变化。
sourcemonitor-details-pta4
sourcemonitor-details-pta5
sourcemonitor-details-pta6
从表中可以看出,作业集 5 相比作业集 4,类数量从 9 增加到 12,说明第二版已经开始把不同逻辑元件拆成独立类型;同时分支数量从 73 增加到 162,说明元件种类和管脚规则明显变复杂。作业集 6 的有效代码行达到 641 行,分支数量进一步增加到 199,主要原因是引入了子电路解析、连接合法性检查、端口展开和输出排序等逻辑。
这组数据也暴露出一个问题:虽然类数量有增加,但 Main 类承担的责任仍然过重。尤其第三版中,Main 同时负责读取输入、解析字符串、检查异常、展开子电路、模拟电路、排序输出,导致代码虽然能够覆盖大部分测试点,但局部逻辑之间耦合较强,后续维护成本偏高。
2.2 作业集 4:基础逻辑门与级联计算
作业集 4 的核心任务是根据输入信号和连接关系,模拟 AND、OR、NOT、XOR、XNOR 等基础门电路的输出。第一版代码中设计了 Component 接口,并由 AndGate、OrGate、NotGate、XorGate、XnorGate 等类分别实现。Main 类负责读取 INPUT 行和连接行,将输入信号保存为 ExternalInput,将连接关系保存为 Connection,再通过循环传播信号值,直到没有新的变化。
图 1 是第一版代码的类结构示意。
pta4-class
第一版流程可以概括为:先读取输入信号,再读取连接关系;在读取连接关系时识别元件并创建对象;随后把外部输入信号作为初始值,不断沿 Connection 传播;当某个 Component 的输入全部已知时调用 compute 计算输出;最后按照门类型和编号排序输出。
pta4-flow
这一版的优点是能够比较直接地把“逻辑门”抽象成对象,使用多态屏蔽不同门的计算细节;缺点也比较明显:Connection 的方向判断依赖输入字符串中 token 的位置,元件创建又和连接解析过程绑在一起,导致一旦出现更复杂的连接形式,代码很容易漏建元件或错误识别输入输出。测试结果中,前 39 个测试点均通过,但 case40“多层级联电路”出现“非零返回”,说明程序在复杂级联场景下存在未处理的运行时风险。
失败测试点 类型 内存 时间 状态 得分
case40 多层级联电路 19996 KB 100 ms 非零返回 0/5

结合第一版源码结构分析,非零返回的根本原因不是某个逻辑门的真值表写错,而是整体解析模型不够稳固。第一版代码中存在较多 substring、indexOf、split 和数组下标操作,一旦输入 token 的格式或连接层级超出预期,就可能出现 StringIndexOutOfBoundsException、NumberFormatException 或空对象访问等问题。也就是说,程序在简单测试下表现正常,但对于深层级联和边界输入缺少足够防御。
2.3 作业集 5:扩展元件与统一管脚模型
作业集 5 在基础门的基础上加入了更复杂的元件,例如三态门、译码器、数据选择器、数据分配器等。相比第一版,第二版的重要改进是引入抽象类 Component,把不同元件共有的 name、type、param、number、ctrlPins、inPins、outPins、pinVals 等字段统一到父类中。每个子类只需要实现 setupPins、compute、isValidOutput、formatOutput 等方法。
图 2 是第二版代码的类结构示意。
pta5-class
第二版相比第一版最大的进步,是把“管脚”作为统一模型进行处理。基础门、三态门、译码器等元件虽然输入输出数量不同,但都可以用 ctrlPins、inPins、outPins 和 pinVals 表示。这样,Main 类在传播信号时不需要知道某个元件具体是什么类型,只需要根据连接关系把源元件的输出管脚值写入目标元件的输入管脚即可。
从测试结果看,第二版 34 个测试点全部通过,最终得分 100/100。
测试范围 通过情况 总分
基本元件测试 全部通过 34/34 个测试点
元件组合测试 全部通过 100/100
复杂电路 全部通过 100/100

这一版的成功说明统一管脚模型是有效的。它降低了不同元件之间的差异,使程序可以用同一套模拟流程处理多种元件。但是,从可维护性角度看,这一版仍然留下了两个隐患:第一,未知值和高阻态用 -1、2 等数字表示,属于典型的“魔法值”,后续阅读和修改时容易混淆;第二,Main 类仍然承担了解析、创建、连接、模拟和输出等多个职责,类之间的职责边界还不够清晰。
2.4 作业集 6:子电路、异常判断与复杂展开
作业集 6 的重点从“元件本身”转向“电路结构”。这一版引入了子电路定义,输入中可能出现 C1、C2 等子电路块,每个子电路有自己的 INPUT、OUT 和内部连接。主电路中可以引用子电路端口,因此程序需要先解析子电路,再把子电路展开到主电路中,并保证不同子电路内部元件不会发生命名冲突。
第三版代码新增了 SubCircuit、RawConn、Conn、PinRole 等类型。SubCircuit 保存子电路编号、输入端口、输出端口、原始连接和内部元件;RawConn 保存原始连接文本、token 列表、所在子电路和输入顺序,用于异常检查时按原始顺序定位问题;Conn 保存展开后的真实连接;PinRole 用于标记一个 token 是 SOURCE、DEST 还是 UNKNOWN。
图 3 是第三版代码的核心类结构示意。

pta6-class

第三版的主流程可以概括为如下几个步骤。
pta6-flow

这一版的关键在于两个函数:checkErrors 和 flatten。checkErrors 负责检查一条连接中是否有多个输入、没有输入、没有输出、输入输出顺序错误以及输入信号冲突。flatten 则负责把子电路端口转换为带前缀的内部节点,例如把 C1 中的 N1 展开为 C1-N1,以避免不同子电路中同名元件相互冲突。
从测试结果看,第三版的异常类测试点全部通过,说明异常判断的基本方向是正确的;错误集中在子电路场景,具体如下。
失败测试点 类型 内存 时间 状态 得分
case8 多子电路 19888 KB 132 ms 答案错误 0/2
case24 单子电路 19956 KB 134 ms 答案错误 0/2
case29 多子电路 20336 KB 144 ms 答案错误 0/3

这三个错误说明,第三版不是完全无法处理子电路,而是在部分端口映射、输出传播或排序规则上仍有遗漏。结合源码可以看到,程序使用 usedSCs 收集主电路中直接引用的子电路,再在 flatten 中对子电路进行展开。这种做法能够处理一部分简单子电路,但如果子电路之间存在间接连接、端口作为中间节点继续传播、或者输出端口与主电路目标之间需要保留更完整的路径信息,就容易出现输出缺失或输出顺序不符合预期的问题。
我在本地也用一个简单的两级子电路样例进行了调试,输出为:
本地调试样例 输出
C1 中 N1 取反,C2 中 N2 再取反 C1-N1-0:1,C2-N2-0:0

这个结果说明简单的子电路展开是可以工作的,但测试集中更复杂的 case8、case24、case29 仍然暴露了模型不完整的问题。也就是说,第三版的错误不是基础逻辑门计算错误,而是子电路展开规则没有完全覆盖题目所有场景。
三、采坑心得
3.1 字符串解析不能代替稳定的数据模型
第一次作业中,我大量依赖 split、substring、indexOf 等方式处理输入字符串。对于格式简单的 INPUT 行和连接行,这种方式可以快速实现功能;但是当连接关系变复杂时,直接操作字符串很容易隐藏边界问题。例如 A(2)1-0、N1-1、C1-A 这些 token 看起来都包含横线,但它们表达的含义并不相同:有的是元件管脚,有的是子电路端口,有的是外部信号。仅凭字符串位置和是否包含横线来判断输入输出角色,可靠性明显不足。
这也是作业集 4 最后一个测试点出现非零返回的重要原因。程序通过了基础门、简单级联、复杂连接等大部分测试点,但在多层级联中出现运行时错误,说明问题不是某个局部真值表,而是解析过程和连接模型缺少边界保护。后续改进中,第三版增加了 parsePin、pinNumber、pinBase、classify 等函数,实际上就是在弥补第一版中“直接解析字符串”的缺陷。
3.2 元件创建时机影响后续传播
第一版代码在读取连接时创建元件,但主要从连接的输入端 token 中识别元件。这样做有一个隐患:如果某个元件第一次出现时是作为源端出现,或者它的输出端被用作后续连接源,就可能发生漏建或延迟创建。对于简单电路,这个问题不明显;对于多层级联,源端和目标端的关系会交替出现,漏建一个元件就可能导致后续信号无法传播,最终输出缺失或程序异常。
第二版和第三版中,元件识别不再只依赖某个固定位置,而是对连接中的 token 做更完整的扫描,并通过 createComponent 和 pinBase 创建元件对象。这一改动虽然增加了代码量,却让程序对输入顺序的依赖降低了许多。
3.3 统一管脚模型是必要的,但魔法值会带来新问题
作业集 5 之所以能全部通过,一个关键原因是引入了统一管脚模型。不同元件都使用 pinVals 保存管脚值,使用 ctrlPins、inPins、outPins 区分控制端、输入端和输出端。这使得三态门、译码器、数据选择器、数据分配器可以进入同一套模拟流程。
但是,第二版仍然用 -1 表示未知值,用 2 表示高阻态。这种设计在代码量较小时可以接受,但当后续加入异常判断和子电路展开后,判断条件会变得分散。例如输出时要排除 -1 和 2,传播时也要排除 -1 和 2,计算时又要区分“未知”和“合法但不输出”的状态。如果后期某个分支忘记处理 2,就可能产生错误输出。因此我认为更好的做法是定义枚举或常量,例如 UNKNOWN、LOW、HIGH、HIGH_Z,而不是直接在代码中写数字。
3.4 子电路展开的本质是图问题
作业集 6 给我的最大提醒是:子电路不能只当成字符串前缀处理。给内部元件加 C1-、C2- 前缀只能解决命名冲突,却不能自动解决端口映射、路径连通和输出规则。子电路的 INPUT 和 OUT 本质上是图中的端口节点,内部元件是图中的计算节点,连接关系是有向边。只有把这些元素都作为图节点处理,才能稳定支持单子电路、多子电路以及可能出现的间接连接。
第三版中 flatten 函数已经开始使用 Map<String,List> 构造图,并通过 DFS 从真实源点寻找目标输入管脚。但从 case8、case24、case29 的答案错误看,这个图模型还不够完整。问题可能出现在三类场景:第一,子电路输出端作为中间节点继续连接到其他目标时,边的保留不完整;第二,主电路只收集直接使用的子电路,若存在更深层的间接引用,usedSCs 可能不完整;第三,输出排序规则同时涉及子电路编号、元件类型和编号,现有排序逻辑可能与题目要求仍有差异。
3.5 异常优先级要用原始输入顺序保证
作业集 6 中异常类测试点全部通过,说明第三版在异常检查上的思路基本正确。RawConn 类保存了连接原文、token 列表、所属子电路和原始顺序,checkErrors 先按输入顺序检查每一条连接,再判断多个输入、无输入、无输出、输入输出顺序错误和输入冲突。这种设计比临时在模拟过程中发现异常更可靠,因为异常输出往往要求按照题目规定的优先级和原始文本格式输出。
这里的心得是:异常处理不应该只是 try-catch。对于 PTA 这类严格比较输出的题目,异常本身也是业务规则的一部分,需要进入数据模型。哪些 token 是源,哪些 token 是目标,哪个目标已经被其他源驱动,都需要明确记录,而不能等到程序崩溃后再处理。
3.6 提交前必须本地编译和构造针对性样例
桌面最终版 Main.java 和 AbstractCoursePTA/Main.java 均已通过本地 javac 编译检查。对于本轮作业,单纯依赖平台测试是不够的,因为一次提交失败后很难直接看到隐藏测试数据。更有效的方法是根据失败类型自己构造小样例。例如第三版中,我用两个子电路连续取反的样例验证了基本展开流程,确认简单子电路可以输出 C1-N1-0:1 和 C2-N2-0:0,再把排查重点放到更复杂的端口连接上。
此外,桌面“使用说明.txt”中也提醒过,快速输入超过 300 行代码时可能出现换行位置问题。本轮代码从第一版到第三版已经超过 300 行,最终有效代码行达到 641 行。如果提交过程中因为复制、注入或换行问题导致某个括号、分号、字符串被破坏,就会出现与源码逻辑无关的编译或运行错误。因此,提交前至少要完成三步:本地 javac 编译、运行一组自定义样例、检查平台粘贴后的代码结构是否完整。
四、改进建议
第一,应该把字符串解析结果封装为明确的对象。当前程序中 token 仍然以 String 形式在多个函数之间传递,导致 classify、pinNumber、pinBase、subToken 等函数反复解析同一个字符串。后续可以设计 PinRef 类,字段包括 rawText、scope、componentName、pinNumber、isSubCircuitPort、role 等。这样一条连接可以解析为 ConnectionDef,而不是在每个阶段重新 split。
第二,应该用枚举替代魔法值。信号值可以定义为 Signal.LOW、Signal.HIGH、Signal.UNKNOWN、Signal.HIGH_Z,元件输出时根据枚举判断是否有效。这样可以避免 -1 和 2 在不同位置含义不清的问题,也能使三态门和普通逻辑门的输出规则更直观。
第三,应该拆分 Main 类职责。即使 PTA 要求所有代码放在 Main.java 中,也可以在同一个文件中划分多个类:Parser 负责读取和解析输入,ComponentFactory 负责创建元件,ErrorChecker 负责异常检查,CircuitGraph 负责保存节点和边,Simulator 负责信号传播,OutputFormatter 负责排序和输出。Main 只做流程编排。这样既不违反提交限制,又能明显降低主函数复杂度。
第四,子电路展开应该改为递归图展开。当前 usedSCs 主要从主电路连接中收集子电路编号,这种方式对简单引用有效,但对复杂子电路关系不够稳。更好的做法是从主电路作为根开始,递归展开所有被引用的子电路,并为每一层维护命名空间栈。端口节点不应该被简单替换成字符串,而应该作为图中的正式节点参与连边。
第五,模拟过程可以从全量迭代改为事件驱动。当前 simulate 中每轮会遍历所有元件和所有连接,复杂度接近 O(迭代次数 × 元件数 × 连接数)。当电路规模增大时,这种方式效率较低。可以维护一个“值发生变化的管脚队列”,只有源管脚变化时才传播到相邻目标,从而减少无效计算。
第六,测试要覆盖“单元测试、组合测试、异常测试、回归测试”四类。基础门元件可以单独测试真值表;复杂元件可以测试典型控制端组合;子电路可以测试单子电路、多子电路、端口直连、输出接输入、未使用端口等场景;对于已经失败过的 case8、case24、case29,应当构造类似结构作为回归测试,避免修复一个问题后又破坏其他场景。
第七,输出排序规则要独立封装。第三版中输出排序同时比较子电路编号、元件类型、元件编号和名称字符串。排序规则一旦散落在主流程中,后期很难检查。建议单独设计 Comparator,并用若干样例验证排序结果,特别是 C1-N1、C2-N1、A(2)1、O(3)2 等混合名称的顺序。
五、总结
通过作业集 4~6,我最大的收获是对面向对象建模有了更具体的认识。以前理解“封装、继承、多态”时,更多停留在语法层面;这轮作业让我意识到,抽象类和接口不是为了让代码看起来高级,而是为了在需求继续扩展时,让新增功能有地方放。作业集 5 使用统一 Component 抽象后,扩展元件明显比第一版顺畅,这就是合理抽象带来的收益。
同时,我也认识到自己在复杂问题建模方面还有不足。作业集 6 的 93/100 说明程序已经覆盖了大多数场景,但没有完全理解子电路作为图结构的本质。当前代码能够通过异常测试和多数子电路测试,却在少数单子电路、多子电路测试中出错,说明模型边界仍然存在漏洞。以后遇到类似问题时,我应该先画出节点、边、端口和作用域关系,再写代码,而不是一边写解析一边补特殊情况。
本阶段还让我更加重视数据化复盘。仅仅说“这次题目很难”没有意义;把三次提交的代码行数、类数量、分支数量、测试点通过情况列出来后,才能看出问题在哪里:第一版代码规模较小但模型脆弱,第二版通过抽象统一元件后效果最好,第三版功能最多但复杂度也最高,错误集中在子电路展开。这样的复盘比单纯记录“哪里错了”更有价值。
后续我需要继续学习和加强的方面主要有三点:一是 UML 类图和流程图表达能力,要能在写代码前先把结构设计清楚;二是 Java 集合、图算法和递归建模能力,特别是带命名空间的图展开;三是测试意识,不能只依赖平台隐藏测试,而要主动构造覆盖边界条件的样例。只有把这些能力补上,后续面对更大规模的程序时,才能写出不仅能通过测试,而且结构清晰、便于维护的代码。
总的来说,作业集 4~6 是一次比较完整的迭代训练:从基础门电路开始,到扩展元件,再到子电路和异常规则。我的代码虽然还存在不足,但这轮作业让我更清楚地看到了程序设计中“模型先行”的重要性。接下来我会在保证功能正确的基础上,更重视结构设计、边界处理和可持续改进。

posted @ 2026-06-23 12:11  李大侠盗猎  阅读(2)  评论(0)    收藏  举报