PTA题目集4~6的总结性Blog

· 前言

本次的三个作业,由答题判题程序- 4、家居强电电路模拟程序- 1、家居强电电路模拟程序 -2组成。
答题判题程序-4是对前三次判题程序的最后升级,设计多个子类继承于基础题类来实现对每种题型的判断和计算分值;而家居强电电路模拟程序-1则是对输入的各个设备在串联关系中的状态更新,其中涉及到设备电压的计算和开关状态的判断,以及受控设备的状态输出;最后家居强电电路模拟程序-2则在前一题的基础上增加了并联的内容,在逻辑处理上更加的复杂了。本文将系统性总结这三次题目集的知识点、题量及难度,并分析其中的核心内容和实现方法。

· 题目集概述

· 答题判题程序 - 4:

题目分析:

  1. 数据结构
    题目信息、试卷信息、学生信息、答卷信息分别存储为字典或列表,方便检索与处理。
    单选题、多选题、填空题设计为子类单独处理判题逻辑。

  2. 输入处理
    根据输入前缀(如#N:、#T:)判断当前行信息类型,逐一解析存储。
    本次作业新增输出顺序变化:
    只要是正确格式的信息,可以以任意的先后顺序输入各类不同的信息。比如试卷可以出现在题目之前,删除题目的信息可以出现在题目之前等。
    要成功处理以上要求就应该将题目、试卷等信息先
    本次作业新增输入格式内容:
    多选题:格式:"#Z:"+题目编号+" "+"#Q:"+题目内容+" "#A:"+标准答案
    填空题:格式:"#K:"+题目编号+" "+"#Q:"+题目内容+" "#A:"+标准答案
    格式基本的约束与一般的题目输入信息一致。 例如:#K:2 #Q:古琴在古代被称为: #A:瑶琴或七弦琴
    删除题目信息需在存储中直接删除,同时记录。

  3. 判题计算分数规则
    单选题:答案完全匹配为正确,否则为错误。
    多选题:所有正确选项被选择且无错误选项给满分;部分正确无错误选项得半分;错误或无答案得0分。
    填空题:答案完全匹配为正确;部分匹配得半分;错误或无答案得0分。

  4. 输出处理
    按学号与试卷号排序输出。
    输出每道题的详细判定结果和学生总分。
    若题目回答正确则输出true,多选题或者填空题部分正确输出~partially correct,单选题没有部分正确的情况;
    若答案错误或者包含错误选项或者内容则输出~false。

  5. 警告信息
    如果试卷总分不为100,输出“alert: full score of test paper1 is not 100 points”
    如果试卷不存在,输出“the test paper number does not exist”
    如果试卷错误地引用了一道不存在题号的试题,在输出学生答案时,提示”non-existent question~”加答案。
    如果该题号被删除并且在答卷中没有答案则只输出answer is null


设计与分析

  1. 正则表达式

  • 使用正则表达式匹配输入格式:匹配题目格式(#N, #Z, #K 等)。匹配试卷信息格式(#T)。匹配答卷信息格式(#S)。
  • Pattern:定义正则表达式。
  • Matcher:执行匹配操作。
  1. 数据结构与集合框架

集合类型

  • 使用 Map(HashMap 和 TreeMap)存储数据:
  • Map<Integer, List> testPapers 用于存储试卷题目及分数。
  • Map<Integer, List> answerSheets 用于存储学生答卷。
  • 使用 List(ArrayList)存储问题、答卷等顺序数据。
  • 使用 Set(HashSet)处理多选题答案的集合操作。

排序

  • 使用 Comparator 对答卷按试卷号排序。
  • 使用 TreeMap 对 answerSheets 按学生 ID 进行排序。

流操作

  • 使用 stream 和 mapToInt 方法计算试卷总分。
  1. 面向对象设计

封装

  • 将不同的功能模块封装为独立的类(如 InputHandler, OutputHandler 等)。使用私有字段和公共方法访问数据。

继承与代码复用

  • 通过继承 Question 类实现不同题型的共同属性与行为(如 getAnswerCorrectnessLevel() 方法)。

多态

  • 使用多态统一调用calculateScore() 等方法。

组合

  • 类中包含其他类的对象,例如 AnswerSheet 包含 QuestionScore 和学生答案。

单一职责

  • InputHandler 专注于输入数据解析。
  • OutputHandler 专注于处理输出。

源码结构分析

  • 设计模式:
    继承与多态:题目类型(单选、多选、填空)继承自 Question,实现不同的行为。
    组合模式:Exam 包含题目集合,AnswerSheet 包含题目分数列表。
    分层设计:输入、逻辑处理、输出各自分离,增强模块化。
    类间关系:
  • Main 负责调用。
    Exam 作为核心类管理题目。
    AnswerSheet 结合 Exam 和 Question 进行答案校验与得分计算。
    InputHandlerOutputHandler 负责与外界的交互。


main类中调用:
inputHandler.readExamData(exam, testPapers, answerSheets, testIds, students);来处理用户输入

点击查看代码
    // 读取题目、试卷和答卷数据
    public void readExamData(Exam exam, Map<Integer, List<QuestionScore>> testPapers, Map<Integer, List<AnswerSheet>> answerSheets, List<Integer> testIds, Map<Integer, Student> students) {
        while (true) {
            String inputLine = scanner.nextLine();
            if (inputLine.equals("end")) {
                break;  // 输入结束
            }
            // 解析题目信息
            try {
                if (inputLine.startsWith("#N:")) {
                    Pattern questionPattern = Pattern.compile("#N:(\\d+)\\s+(?:(#Q:(.+?))\\s+#A:(.+?))");
                    Matcher matcher = questionPattern.matcher(inputLine);
                    if (matcher.matches()) {
                        int num = Integer.parseInt(matcher.group(1));

                        String questionContent, standardAnswer;

                        // 判断匹配的顺序并获取对应的 group
                        if (matcher.group(3) != null) {
                            // #Q 在前
                            questionContent = matcher.group(3);
                            standardAnswer = matcher.group(4);
                        } else {
                            // #A 在前
                            questionContent = matcher.group(6);
                            standardAnswer = matcher.group(5);
                        }

                        exam.addQuestion(num, new BasicQuestion(num, questionContent, standardAnswer));
                    } else {
                        System.out.println("wrong format:" + inputLine);
                    }
                }                // 解析多选题
                else if (inputLine.startsWith("#Z:")) {
                    Pattern mcPattern = Pattern.compile("#Z:(\\d+)\\s+#Q:(.+?)\\s+#A:(.+)");
                    Matcher matcher = mcPattern.matcher(inputLine);
                    if (matcher.matches()) {
                        int num = Integer.parseInt(matcher.group(1));
                        String questionContent = matcher.group(2);
                        String standardAnswer = matcher.group(3);
                        exam.addQuestion(num, new ChoiceQuestion(num, questionContent, standardAnswer));
                    } else {
                        System.out.println("wrong format:" + inputLine);
                    }
                }

                // 解析填空题
                else if (inputLine.startsWith("#K:")) {
                    Pattern fbPattern = Pattern.compile("#K:(\\d+)\\s+#Q:(.+?)\\s+#A:(.+)");
                    Matcher matcher = fbPattern.matcher(inputLine);
                    if (matcher.matches()) {
                        int num = Integer.parseInt(matcher.group(1));
                        String questionContent = matcher.group(2);
                        String standardAnswer = matcher.group(3);
                        exam.addQuestion(num, new FillInTheBlankQuestion(num,questionContent, standardAnswer));
                    } else {
                        System.out.println("wrong format:" + inputLine);
                    }
                }


                // 解析试卷信息
                else if (inputLine.startsWith("#T:")) {
                    Pattern testPattern = Pattern.compile("#T:(\\d+) ((\\d+-\\d+ ?)+)");
                    Matcher matcher = testPattern.matcher(inputLine);
                    if (matcher.matches()) {
                        int testPaperId = Integer.parseInt(matcher.group(1));
                        List<QuestionScore> paperQuestions = new ArrayList<>();
                        String[] parts = matcher.group(2).split(" ");
                        for (String part : parts) {
                            String[] tValues = part.split("-");
                            int questionNum = Integer.parseInt(tValues[0]);
                            int score = Integer.parseInt(tValues[1]);
                            paperQuestions.add(new QuestionScore(questionNum, score));
                        }
                        testPapers.put(testPaperId, paperQuestions);
                        int totalScore = paperQuestions.stream().mapToInt(QuestionScore::getScore).sum();
                        if (totalScore != 100) {
                            System.out.println("alert: full score of test paper " + testPaperId + " is not 100 points");
                        }
                    } else {
                        System.out.println("wrong format:" + inputLine);
                    }
                }

                // 解析学生信息
                else if (inputLine.startsWith("#X:")) {
                    Pattern studentPattern = Pattern.compile("#X:(.+)");
                    Matcher matcher = studentPattern.matcher(inputLine);
                    if (matcher.matches()) {
                        String[] parts = inputLine.substring(3).split("-");
                        for (String part : parts) {
                            String[] studentData = part.split(" ");
                            int studentId = Integer.parseInt(studentData[0].trim());
                            String studentName = studentData[1].trim();
                            students.put(studentId, new Student(studentId, studentName));
                        }
                    } else {
                        System.out.println("wrong format:" + inputLine);
                    }
                }

                // 解析答卷信息
                else if (inputLine.startsWith("#S:")) {
                    Pattern answerPattern = Pattern.compile("#S:(\\d+) (\\d+)(\\s+(#A:\\d+-(.+)*)*)*");
                    Matcher matcher = answerPattern.matcher(inputLine);
                    if (matcher.matches()) {
                        String[] parts = inputLine.split("#");
                        int testPaperId = Integer.parseInt(parts[1].split(" ")[0].split(":")[1].trim());
                        int studentId = Integer.parseInt(parts[1].split(" ")[1].trim());  // 提取学号
                        AnswerSheet answerSheet = new AnswerSheet(testPaperId);
                        if (parts.length > 2) {
                            for (int i = 2; i < parts.length; i++) {
                                if (parts[i].startsWith("A:")) {
                                    String[] answerParts = parts[i].split("-");
                                    if (answerParts.length == 2) {
                                        int questionNum = Integer.parseInt(answerParts[0].split(":")[1].trim());
                                        String str = answerParts[1]; // 去除可能的空格
                                        String answer = removeLastSpace(str);
                                        answerSheet.addAnswer(questionNum, answer);
                                    }
                                }
                            }
                        }

                        answerSheets.computeIfAbsent(studentId, k -> new ArrayList<>()).add(answerSheet);  // 将答卷关联到学生
                    } else {
                        System.out.println("wrong format:" + inputLine);
                    }
                }

                // 解析删除题目信息
                else if (inputLine.startsWith("#D:N-")) {
                    Pattern deletePattern = Pattern.compile("#D:N-(\\d+)");
                    Matcher matcher = deletePattern.matcher(inputLine);
                    if (matcher.matches()) {
                        int questionNum = Integer.parseInt(inputLine.split("-")[1].trim());
                        exam.removeQuestion(questionNum);  // 移除题目
                    }
                    else {
                        System.out.println("wrong format:" + inputLine);
                    }
                }
            } catch (Exception e) {
                System.out.println("wrong format");
            }

        }
    }

该函数 readExamData 用于从输入中解析并加载试卷系统的相关数据,包括题目、试卷、答卷和学生信息。它通过扫描输入的每一行,根据不同的前缀(如 #N:#T: 等)区分处理不同类型的数据。

解析的逻辑主要包括以下部分:

  • 题目数据解析
    针对不同类型的题目(普通题、多选题、填空题等),使用正则表达式提取题目编号、题干、标准答案等信息。根据题目类型创建相应的题目对象(如 BasicQuestionChoiceQuestion 等),并将其添加到考试对象中。输入格式错误会输出提示。

  • 试卷信息解析
    试卷数据以 #T: 开头,提取试卷编号及其对应的题目编号和分值。分值被封装为 QuestionScore 对象,并存入 testPapers 映射中。同时校验总分是否为 100 分,否则会警告。

  • 学生信息解析
    #X: 开头的输入解析学生编号和姓名,将其存入 students 映射中,关联学生 ID 和学生对象。

  • 答卷信息解析
    #S: 开头的输入解析学生提交的答卷,包括试卷编号、学生编号及其题目作答信息,将答卷与学生对应关系存储到 answerSheets 中。

  • 删除题目
    #D:N- 开头的输入解析需要删除的题目编号,从考试中移除对应题目。

函数通过逐行处理输入,使用正则表达式确保数据格式的正确性,并对异常或格式错误的输入提供警告提示,同时确保将各类数据有序地存储到相应的结构中(如 MapList 等),为试卷系统的后续操作提供数据支持。

AnswerSheet类用于记录考生对某张试卷的答题情况,以及根据试卷内容输出答案详情和得分情况。该类包含了试卷编号、题目信息、考生的答案,以及输出答案和计算得分的逻辑。
outputAnswers方法输出考生的答案以及其对应的正确性。

实现细节:

  • 遍历所有试卷题目(paperQuestions)。
  • 从 Exam 中获取每道题的 Question 实例。
  • 获取考生答案并进行以下判断:
    • 如果试题不存在于试卷中,输出 non-existent question~0;
    • 如果答案为空,输出 answer is null;
    • 如果题目内容无效(如包含 "invalid"),输出内容加 ~0。
  • 调用 Question 的 getAnswerCorrectnessLevel 方法,获取答案的正确性等级,并输出具体结果。

outputScores类输出考生在每道题上的得分以及总分。

实现细节:

  • 初始化 totalScore 和 earnedScore。
  • 遍历所有题目:
    • 如果题目不存在,得分为 0;
    • 如果题目存在,调用 Question 的 calculateScore 方法计算该题得分,并累计到 totalScore。
  • 按题目顺序输出每题得分,用空格分隔,最后输出总分。

时序图


踩坑心得

1. 乱序输入问题
- 问题描述:
比如 #N, #Z, #K, #T, #X, #S 等只要是正确格式的信息,可以以任意的先后顺序输入各类不同的信息。比如试卷可以出现在题目之前,删除题目的信息可以出现在题目之前等。

else if (inputLine.startsWith("#S:")) {
                    Pattern answerPattern = Pattern.compile("#S:(\\d+) (\\d+)( (#A:\\d+-(.+)*)*)*");
                    Matcher matcher = answerPattern.matcher(inputLine);
                    if (matcher.matches()) {
                        String[] parts = inputLine.split("#");
                        int testPaperId = Integer.parseInt(parts[1].split(" ")[0].split(":")[1].trim());
                        int studentId = Integer.parseInt(parts[1].split(" ")[1].trim());  // 提取学号
                        List<QuestionScore> paperQuestions = testPapers.get(testPaperId);
                        AnswerSheet answerSheet = new AnswerSheet(testPaperId, paperQuestions);
                        if (parts.length > 2) {
                            for (int i = 2; i < parts.length; i++) {
                                if (parts[i].startsWith("A:")) {
                                    int questionnum = Integer.parseInt(parts[i].split("-")[0].split(":")[1].trim());
                                    String answer = parts[i].split("-")[1].trim();
                                    answerSheet.addAnswer(questionnum, answer);
                                }
                            }
                        }

                        answerSheets.computeIfAbsent(studentId, k -> new ArrayList<>()).add(answerSheet);  // 将答卷关联到学生
                    }

在上面的代码中进行#S答卷内容的解析,如果按代码的逻辑先获得testPapers.get(testPaperId);就会导致乱序输入试卷在答卷前报错。
解决思路:
AnswerSheet answerSheet = new AnswerSheet(testPaperId);修改AnswerSheet类构造方法,
List<QuestionScore> questionScores = testPapers.get(testPaperId);
answerSheet.setPaperQuestions(questionScores);在输出类中获取questionScores 再将该值赋给该对象。

2. 多选题、填空题判分逻辑问题
- 问题描述:
①如果多选题答卷答案与标准答案部分相同,且没有包含不包括在标准答案中的答案,就判定为部分正确,分数计算为该题分数的一半,多余小数直接舍去。
②如果为填空题同理多选题,但标准答案以或连接多个答案,所以可以根据字符拆分正确答案来计算分值。

解决思路

  1. 多选题:
    @Override
    public boolean isPartiallyCorrect(String answer) {
        String[] correctAnswers = this.standardAnswer.split(" ");
        String[] userAnswers = answer.split(" ");

        Set<String> correctSet = new HashSet<>(Arrays.asList(correctAnswers));
        Set<String> userSet = new HashSet<>(Arrays.asList(userAnswers));

        // 判断用户的答案是否为正确答案的子集,且没有多余选项
        return correctSet.containsAll(userSet) && !userSet.equals(correctSet);
    }
  1. 填空题:
    @Override
    public CorrectnessLevel getAnswerCorrectnessLevel(String answer) {
        if (isCorrect(answer)) {
            return CorrectnessLevel.CORRECT;
        } else if (isPartiallyCorrectForFillIn(answer)) {
            return CorrectnessLevel.PARTIALLY_CORRECT;
        } else {
            return CorrectnessLevel.INCORRECT;
        }
    }

    // 填空题的部分正确判断
    private boolean isPartiallyCorrectForFillIn(String answer) {
        String[] part = standardAnswer.split("或");

        for (String ne : part) {
            if (answer.equals(ne.trim())) {
                return true;
            }
        }
        return false;
    }

这些判断逻辑根据以下结构体来简化操作逻辑:

    public enum CorrectnessLevel {
        CORRECT,
        PARTIALLY_CORRECT,
        INCORRECT
    }

改进建议

  1. 分清职责,结构更清晰
    现在的代码把逻辑都放在一个类里,显得臃肿。可以拆分成负责输入输出、流程控制、题目解析等不同模块,各自只做自己的事。

  2. 减少重复,提升复用性
    解析题目、验证答案时有很多重复代码。把这些重复逻辑抽取成公共方法或工具类,既省事又方便维护。

  3. 代码更易读,少写嵌套
    多用早返回和拆分小方法的方式,避免复杂的 if-else 嵌套,让代码看起来更直观。

· 家居强电电路模拟程序 - 1

题目分析

1. 电路设备分类

设备分为控制设备受控设备两类,每类包含多种具体设备。

  • 控制设备

    • 开关(K)
      • 状态:0(打开/turned on)1(关闭/closed)
      • 功能:控制电压传递,状态为 1 时,输入电压传递到输出端;为 0 时输出端电压固定为 0
    • 分档调速器(F)
      • 档位:03
      • 功能:输入固定电压,通过档位调节输出电压比例(0.30.60.9)。
    • 连续调速器(L)
      • 档位范围:[0.00, 1.00],精确到两位小数。
      • 功能:输出电压为档位值与输入电压的乘积。
  • 受控设备

    • 白炽灯(B)
      • 工作状态:亮(0~200lux)或灭。
      • 功能:根据电压差计算亮度(线性比例)。
    • 日光灯(R)
      • 工作状态:亮度为180lux0lux
      • 功能:仅取决于是否有电压差。
    • 吊扇(D)
      • 工作状态:停止或转动(转速范围 0~360 转/分钟)。
      • 功能:根据电压差线性调整转速,低于 80V 停止。
2. 电路规则
  1. 电压传递规则
    • 开关决定电压传递状态。
    • 调速器通过档位或比例控制输出电压。
  2. 连接规则
    • 串联方式,电压从电源依次传递。
    • 所有设备需严格按照物理规律接入(如无反馈、并联等复杂情况)。
  3. 输入输出规则
    • 所有设备连接以 VCC 为起点,GND 为终点。
    • 输入无连接的引脚默认为接地(0V)。
  4. 设备编号与状态输出
    • 同种设备按编号顺序依次输出状态。
3. 输入与输出
  • 输入内容
    1. 设备连接信息。
    2. 控制设备调节信息。
    3. 电路结束标志(end)。
  • 输出内容
    • 所有设备的状态或参数值,按设备类型和编号顺序输出。
4. 功能实现核心
  1. 设备状态更新
    • 根据连接关系和调节操作动态更新每个设备的状态。
  2. 电压传递与计算
    • 模拟串联电路中电压的分布和传递。
    • 处理调速器对电压的调节和受控设备的状态计算。

运行逻辑分析

1. 初始化设备
  • 程序通过继承和多态设计了多种设备类型,包括电源(VCC)、接地(GND)、开关(Switch)、分档调速器(StepSpeedController)、连续调速器(ContinuousSpeedController)、灯(白炽灯和日光灯)、风扇等。
  • 这些设备通过继承基类 Device,实现了多态行为,比如输入电压的设置和状态更新。
  • 特殊设备(如 VCCGND)在程序启动时被初始化。

2. 解析输入
  • 程序从标准输入中读取指令,分为两类:
    • 连接关系[设备1-引脚 设备2-引脚]):通过 parseConnection 方法将设备连接信息解析成 Device 对象,并将连接关系存储到 Circuit 中。
    • 控制命令#设备编号:参数):如开关状态切换、调速器调整等,这些命令被存储到 commands 列表中。

3. 建立电路连接
  • 所有设备通过 connect 方法连接,形成设备链。DevicenextDevice 属性记录下一个设备。
  • 特殊处理:
    • 如果某设备连接到 GND,会调用 Ground.setPreviousDevice,设置电压为零。

4. 执行命令
  • 调用 executeCommands 方法,解析存储的命令并对设备进行操作:
    • 切换开关状态(如开关打开或关闭)。
    • 调整分档调速器的档位。
    • 设置连续调速器的电压比例。
  • 在操作结束后,电路从 VCC 开始,通过 setVoltage 触发电压向后传递,最终更新所有设备的状态。

5. 状态输出
  • 调用 printStatus 方法,根据设备类型输出状态,包括:
    • 开关的开关状态。
    • 调速器的档位。
    • 灯的亮度。
    • 风扇的转速。
  • 每种设备的状态通过其自身的 updateState 方法根据输入电压更新。

知识点总结

1. 面向对象编程 (OOP)
  • 抽象类Device 是所有设备的基类,定义通用属性和方法(如 inputVoltageconnectTo 等),子类通过继承实现具体行为。
  • 多态:通过 Device 的引用调用子类重写的方法(如 updateState),实现设备的状态更新。
  • 封装:各设备的具体实现细节被封装在子类中,对外提供统一的接口。
2. 继承与类层次设计
  • 程序利用继承,设计了清晰的类层次:
    • Device 是基类。
    • ControlDeviceControlledDevice 是子类,分别表示控制型设备和受控型设备。
    • 各种具体设备(如 SwitchFan 等)再进一步继承上述子类。
3. 电路仿真逻辑
  • 电压传递:设备通过 connectTo 方法形成链式连接,电压从电源 VCC 开始逐级传递给后续设备。
  • 状态更新:每个设备的状态由输入电压和自身的逻辑决定。
4. 数据结构
  • Map 存储设备:通过设备名称作为键,存储所有设备实例。
  • List 存储连接关系:记录设备间的连接信息。
  • List 存储命令:保存用户输入的操作指令。
5. 输入处理
  • 使用字符串解析连接信息和控制命令。
  • 运用了 String.split 方法对输入进行拆分,并对输入格式进行了简单验证。
6. 流式操作与排序
  • 利用 Java 8 的流式操作对设备按名称排序后输出状态,代码简洁高效。
7. 异常处理与约束
  • 设置电压、电流、亮度等参数时,对输入值进行了范围限制,确保模拟行为的合理性。

源码结构分析

1. 核心模块

  • Device:所有设备的抽象基类,定义了设备的通用属性和行为(如输入电压、输出电压、连接关系等)。
  • 子类划分
    • 电源与接地
      • VCC:电路电源,起始设备,负责提供固定电压。
      • Ground:电路接地,终止设备,确保电压差为0。
    • 控制设备
      • Switch:控制电路通断。
      • StepSpeedController:分档调速器。
      • ContinuousSpeedController:连续调速器。
    • 受控设备
      • IncandescentLamp:白炽灯,亮度根据电压线性变化。
      • FluorescentLamp:日光灯,亮度仅两种状态(180lux或0)。
      • Fan:风扇,转速根据电压非线性变化。

2. 电路管理模块

  • Circuit
    • 负责管理设备集合和连接关系。
    • 提供设备连接、命令执行、状态输出的功能。
  • 方法设计
    • addDevice:添加设备到集合。
    • connect:记录设备连接关系。
    • setConnections:根据连接信息建立设备之间的串联关系。
    • executeCommands:执行控制命令,调整设备状态。
    • printStatus:打印设备状态。

3. 主程序模块

  • Main
    • 处理用户输入。
    • 调用Circuit类的方法完成电路搭建与操作。

4. 类图设计如下:

以下是程序中几个主要方法的讲解,包括它们的功能、实现原理及作用:


1. Circuit.setConnections()

功能:
将所有已添加的设备按照连接关系(connect 方法记录)构建实际的电路。
实现逻辑:

  • 遍历 connections 列表,逐个取出两个引脚之间的连接关系(pin1pin2)。
  • 确定 pin1 对应的设备与 pin2 对应的设备之间的连接:
    • 如果 pin2 是接地设备(Ground),通过 Ground.setPreviousDevice() 设置 pin1 为其上一个设备,并设置电压。
    • 如果是普通设备,则通过设备的 connectTo 方法建立连接。

作用:

  • 将用户输入的电路描述翻译为程序内部的设备连接关系,为后续电压传递和逻辑控制提供支持。
    关键代码:
for (String[] connection : connections) {
    String pin1 = connection[0].split("-")[0];
    String pin2 = connection[1].split("-")[0];
    if (devices.get(pin2) instanceof Ground) {
        Ground GND = (Ground) devices.get(pin2);
        GND.setPreviousDevice(devices.get(pin1));
    }
    devices.get(pin1).connectTo(devices.get(pin2));
}

2. VCC.setVoltage()

功能:
为电路提供电压源,并向下传递电压到连接的下一个设备。
实现逻辑:

  • 直接将电压值 voltage 传递给 nextDevice
  • 递归更新后续设备的输入电压(通过控制设备或受控设备的逻辑处理)。

作用:

  • 模拟电路中电压从电源流向各设备的过程,开始整个电路的工作。
    关键代码:
public void setVoltage() {
    this.nextDevice.setInputVoltage(voltage);
}

3. Switch.toggleStatus()

功能:
切换开关的状态(开/关)。
实现逻辑:

  • 切换 status 的布尔值。
  • 根据当前状态(true/false)更新开关的输出电压。
  • 输出电压传递给下一个连接的设备(递归传播)。

作用:

  • 提供对开关的基本操作,用户可以通过命令控制电路工作或中断。
    关键代码:
public void toggleStatus() {
    this.status = !this.status; // 切换状态
    updateState();              // 更新设备状态
}

4. StepSpeedController.updateState()

功能:
根据档位(level)调整设备的输出电压,并传递给下一个设备。
实现逻辑:

  • 计算当前档位的电压比例(通过 getVoltageRatio 方法)。
  • 根据比例计算输出电压,传递给连接的下一个设备。
  • 如果连接的是开关,需检查开关状态;否则直接传递电压。

作用:

  • 实现分档调速器对设备输入电压的调节。档位和比例对应实际工程中分段电路设计。
    关键代码:
@Override
void updateState() {
    this.outputVoltage = getVoltageRatio(level) * inputVoltage; // 根据档位比例计算输出电压
    if (nextDevice != null) {
        if (nextDevice instanceof ControlledDevice) {
            ((ControlledDevice) nextDevice).setInputVoltage(outputVoltage);
        } else if (nextDevice instanceof Switch) {
            ((ControlDevice) nextDevice).setInputVoltage(outputVoltage);
        }
    }
}

5. Light.updateState()

功能:
更新灯的亮度,根据电位差(dianshicha)计算亮度值并设置。
实现逻辑:

  • 白炽灯亮度根据电位差线性计算:
    • 0~10V:亮度为 0;
    • 10V~220V:线性计算亮度。
  • 日光灯亮度只有两种状态:
    • 电位差为 0:亮度为 0;
    • 电位差不为 0:亮度为 180lux。

作用:

  • 模拟灯光设备的行为(白炽灯和日光灯特性不同),并体现亮度变化与输入电压的关系。
    关键代码(以白炽灯为例):
@Override
protected void updateState() {
    setDianshicha(); // 计算电位差
    int brightness = 0;
    if (dianshicha < 10) {
        brightness = 0;
    } else if (dianshicha > 10 && dianshicha <= 220) {
        double ratio = (dianshicha - 10) / (220 - 10); 
        brightness = (int) (50 + ratio * 150); 
    }
    this.setBrightness(brightness);
}

6. Circuit.printStatus()

功能:
以指定格式打印各设备的状态,供用户查看电路运行情况。
实现逻辑:

  • 遍历 devices,分别处理不同类型的设备:
    • 开关:打印是否打开;
    • 分档调速器:打印档位;
    • 连续调速器:打印电压比例;
    • 灯:打印亮度;
    • 风扇:打印转速。

作用:

  • 汇总所有设备的运行状态,便于用户调试和分析电路行为。
    关键代码(打印开关状态为例):
devices.values().stream()
    .filter(device -> device instanceof Switch)
    .sorted(Comparator.comparing(device -> device.name))
    .forEach(device -> {
        Switch s = (Switch) device;
        System.out.println("@" + s.name + ":" + (!s.status ? "turned on" : "closed"));
    });

5. 顺序图设计如下:


踩坑心得

问题 1:建立串联设备之间关系的难题

在电路设计中,设备是串联的(比如 VCC -> 开关 -> 灯),更新设备状态时需要沿着串联关系逐一传播电压,但一开始不清楚如何在程序中表达这种连接关系并递归更新状态。

解决方案:
  • 使用属性连接电路上的设备:
    • 每个设备包含一个 nextDevice 属性,用于指向下一个设备。
    • 更新电压时,递归调用 nextDevice 的更新方法。
  • 设计设备基类的接口:
    • 提供统一的 updateState()connectTo(Device nextDevice) 方法。
    • 子类只需在 updateState() 中实现自己的状态逻辑,无需关心全局串联关系。

实现代码示例:

abstract class Device {
    protected Device nextDevice;       // 下一个连接设备
    protected double inputVoltage;    // 当前设备输入电压

    public void connectTo(Device nextDevice) {
        this.nextDevice = nextDevice;  // 建立连接
    }

    // 更新设备状态,子类需要重写
    abstract void updateState();

    public void setInputVoltage(double voltage) {
        this.inputVoltage = voltage;
        updateState(); // 根据输入电压更新自身状态
        if (nextDevice != null) {
            nextDevice.setInputVoltage(this.inputVoltage); // 递归传播电压
        }
    }
}
  • 设备的串联关系通过 connectTo 方法逐步构建。
  • 电压的更新从电源(VCC)向后传播,通过递归调用完成。

问题 2:未正确处理开关的状态对电压传播的影响

假设电路为:VCC -> 白炽灯 -> 开关

  • 如果开关关闭,白炽灯的状态不应受电压影响,但在设计中可能直接将 VCC 的电压传递给白炽灯,忽略了开关状态。
  • 开关的状态应决定电压是否能继续传递到后续设备。
问题分析:
  1. 忽视开关的断路行为:
    • 开关断开时,应阻断电压传播,但可能在代码中直接递归调用后续设备的 setInputVoltage 方法。
  2. 开关行为未被单独抽象:
    • 开关在电路中是特殊的设备,既要管理自己的状态(开/关),又要影响电压的传播。
改进方案:
  • 引入开关逻辑:
    • 开关应判断状态(开/关),仅在开启状态下向下传递电压。
  • 修改电压传播逻辑:
    • Switch.updateState() 方法中,控制是否调用后续设备的 setInputVoltage 方法。
实现代码示例:
class Switch extends Device {
    private boolean status; // 开关状态,true为关闭,false为打开

    public void toggleStatus() {
        this.status = !this.status;  // 切换开关状态
        updateState();
    }

    @Override
    void updateState() {
        if (!status) {  // 如果开关是打开状态
            if (nextDevice != null) {
                nextDevice.setInputVoltage(this.inputVoltage);  // 传递电压
            }
        } else {
            if (nextDevice != null) {
                nextDevice.setInputVoltage(0);  // 阻断电压
            }
        }
    }
}

改进建议

1. 改进串联设备关系的灵活性
  • 问题: 现有的单向链式结构(nextDevice)难以支持复杂电路拓扑(如并联、环路等)。
  • 建议:
    • 使用图或树结构代替单链表,以便更好地描述复杂的电路连接关系。
    • 提供统一的连接接口,支持动态添加或移除设备。

2. 设备状态传播逻辑优化
  • 问题: 电压传播逻辑和设备的工作状态强耦合,重复判断开关等设备的状态,逻辑分散。
  • 建议:
    • 引入统一的电源状态校验机制(如 isPowered() 方法),由设备自主决定是否传播电压或更新状态。
    • 将电源开关的逻辑抽象为独立模块,减少其他设备对其内部逻辑的依赖。

3. 增强扩展性
  • 问题: 不同设备的状态更新逻辑分散在各个类中,扩展新设备需要频繁修改核心代码。
  • 建议:
    • 使用策略模式或配置化设计,将设备的状态更新逻辑解耦为独立模块,便于扩展和维护。
    • 提供统一的设备基类接口,子类仅需实现自己的功能逻辑。

4. 设计上的职责分离
  • 问题: 部分设备(如开关)承担了过多的职责,如既要管理自身状态,还要控制电压传播。
  • 建议:
    • 将职责拆分为更小的模块(如一个专门管理电压传播的控制器)。
    • 开关仅管理自己的开关状态,具体的传播逻辑交由上层处理。

· 家居强电电路模拟程序 - 2

题目分析

与家居强电电路模拟程序 -1相比,这次题目引入了一些新的功能和内容,主要体现在以下方面:

1. 引入并联电路

  • 新增并联电路信息

    • 每条并联电路由若干条串联电路组成,其输入端(IN)和输出端(OUT)分别短接。
    • 并联电路可能包含多个串联电路,且这些串联电路的定义信息需在并联电路定义前输入。
    • 约束:并联电路不包含嵌套的并联电路。
  • 电路结构变化

    • 从单纯的串联电路扩展为包含并联的复杂电路。
    • 并联电路中的所有支路共享相同的输入电压,支路中的设备按电阻分压。

2. 新增受控设备与电阻计算

  • 受控设备引入电阻

    • 白炽灯、电阻为10Ω。
    • 日光灯、电阻为5Ω。
    • 吊扇、电阻为20Ω。
    • 落地扇、电阻为20Ω。
  • 新增电路计算规则

    • 需要根据设备电阻,计算串联电路中每个设备的电压分配。
    • 并联电路中,各支路电压相同,但不同支路的电流需根据设备电阻计算。

5. 串联与并联的混合电路

  • 电路的层次性
    • 总电路可能由串联和并联混合组成,需逐级解析电路结构。
    • 并联电路的每条支路又由多个串联设备构成。

6. 约束与规则

  • 明确了设备连接规则和输入输出规则:

    • 并联设备的输入、输出需正确短接。
    • 总电路的首尾接点固定为 VCC 和 GND。
  • 电路中的默认规则

    • 对未连接的引脚自动接地。
    • 输入电压或电压差不得超过 220V。

功能与实现的新增难点

  1. 并联电路电压统一性

    • 并联电路中每支路需共享相同输入电压,但分担不同的电流。
  2. 串联电路电压分配

    • 考虑设备电阻,动态分配串联设备的电压。
  3. 多种受控设备的状态计算

    • 设备特性多样,需单独设计每种设备的电压与参数映射关系。
  4. 并联电路与串联电路的递归解析

    • 电路解析需支持嵌套逻辑,从上至下分层处理每条电路。

源码结构分析

  1. 输入解析:程序从标准输入中读取命令,解析电路配置和设备控制命令。

    • #T 开头的行用于定义电路拓扑。
    • #M 开头的行用于定义并联电路。
    • #K#L#F 开头的行用于设备控制(如开关、调速器等)。
  2. 设备实例化parseDevices() 方法根据电路配置创建设备实例,并将其添加到电路中。

  3. 计算总电阻caculateResistance()方法通过遍历Circuits表来取出并联电阻和串联电阻,最后计算得出总电阻。

  4. 连接设备setConnections() 方法根据解析的电路拓扑和并联信息,连接各个设备。

  5. 执行命令executeCommands() 方法遍历输入的命令列表,执行相应的设备操作(如切换开关状态、调整调速器档位等)。

  6. 输出状态printStatus() 方法输出各设备的当前状态,如开关状态、灯的亮度、风扇的转速等。

类图设计如下:

以下是程序中几个主要方法的讲解,包括它们的功能、实现原理及作用:

当然,下面是对程序中几个主要方法的详细讲解,包括它们的功能、实现原理、作用,以及关键代码的解释:

parseDevices()

  • 功能:解析电路配置,将每个设备实例化并添加到电路对象中。
  • 实现原理
    • 遍历每个电路的连接信息。
    • 根据设备名称的前缀(如 KFL 等)判断设备类型,并创建相应的设备实例。
    • 使用 switch 语句,根据设备类型创建不同的设备对象(如开关、调速器、灯等)。
    • 将设备实例添加到 circuit 对象中。
  • 关键代码
    for (Map.Entry<String, List<String>> entry : Circuits.entrySet()) {
        List<String> connections = entry.getValue();
        for (String connection : connections) {
            String[] conns = connection.split(" ");
            for (String conn : conns) {
                if (conn.equals("IN") || conn.equals("OUT")) {
                    continue;
                }
                String[] parts = conn.split("-");
                String deviceName = parts[0];
                String deviceType = deviceName.substring(0, 1);
                switch (deviceType) {
                    case "K":
                        circuit.addDevice(new Switch(deviceName));
                        break;
                    case "F":
                        circuit.addDevice(new StepSpeedController(deviceName, 4));
                        break;
                    // 其他设备类型
                }
            }
        }
    }
    
  • 作用:初始化电路中的所有设备,为后续的设备连接和命令执行做好准备。

setConnections(Map<String, List<String>> Circuits, List<String> parallelCircuits)

  • 功能:设置设备之间的连接关系,并计算电路的总电阻。
  • 实现原理
    • 调用 caculateResistance() 方法计算总电阻,考虑串联和并联的组合。
    • 使用 establishConnections() 方法,根据解析的电路拓扑和并联信息,连接各个设备。
    • 根据设备的类型和连接关系,传递电压和电流。
    • 处理并联设备的特性(如电阻的计算和电压分配),并调整电流传递逻辑。
  • 关键代码
    double totalResistance = caculateResistance(Circuits, parallelCircuits);
    VCC vcc = (VCC) devices.get("VCC");
    vcc.setInputVoltage(220.0, totalResistance);
    
    for (String[] conn : connections) {
        String device1 = conn[0];
        String device2 = conn[1];
        if (device1.equals("OUT")) {
            Device device22 = devices.get(device2);
            if (!circuitOpen) {
                double current = vcc.getCurrent();
                if (device22 instanceof ControlledDevice) {
                    ((ControlledDevice) device22).setInputCurrent(current);
                }
            }
        }
    }
    
  • 作用:建立设备之间的物理连接,并设置初始电压和电流条件,为电路的模拟运行提供基础。

executeCommands(List<String> commands)

  • 功能:根据输入命令调整设备状态。
  • 实现原理
    • 遍历命令列表,根据命令类型(开关、调速器)对相应设备进行操作。
    • 例如,切换开关的状态(通过 toggleStatus() 方法),增加或减少调速器的档位(通过 upLevel()downLevel() 方法),设置连续调速器的档位(通过 setLevel() 方法)。
  • 关键代码
    for (String command : commands) {
        if (command.startsWith("#K")) {
            String device = command.substring(1, 3);
            Switch device1 = (Switch) devices.get(device);
            if (device1 != null) {
                device1.toggleStatus();
            }
        } else if (command.startsWith("#F")) {
            String device = command.substring(1, 3);
            StepSpeedController device1 = (StepSpeedController) devices.get(device);
            if (device1 != null) {
                if (command.contains("+")) {
                    device1.upLevel();
                } else if (command.contains("-")) {
                    device1.downLevel();
                }
            }
        }else if (command.startsWith("#L")) {
                  // 设置连续调速器档位.
                  String[] parts = command.split(":");
                  String device = parts[0].substring(1, 3);
                  ContinuousSpeedController device1 = (ContinuousSpeedController) devices.get(device);
                  if (device1 == null) {
                      return;
                  }
                  double value = Double.parseDouble(parts[1]);
                  device1.setLevel(value);
              }
          }
    }
    
  • 作用:动态控制电路中设备的行为,使得模拟电路能够响应用户输入的控制命令。

printStatus()

  • 功能:打印各设备的当前状态。
  • 实现原理
    • 遍历所有设备,根据设备类型打印相应的状态信息。
    • 使用 Java 流和 Comparator 对设备进行排序,以确保输出顺序一致。
    • 信息包括开关状态、调速器档位、灯的亮度、风扇的转速等。
  • 关键代码
    devices.values().stream()
        .filter(device -> device instanceof Switch)
        .sorted(Comparator.comparing(device -> device.name))
        .forEach(device -> {
            Switch s = (Switch) device;
            System.out.println("@" + s.name + ":" + (!s.status ? "turned on" : "closed"));
        });
    
    devices.values().stream()
        .filter(device -> device instanceof StepSpeedController)
        .sorted(Comparator.comparing(device -> device.name))
        .forEach(device -> {
            StepSpeedController sc = (StepSpeedController) device;
            System.out.println("@" + sc.name + ":" + sc.level);
        });
    
  • 作用:提供电路状态的可视化输出,使用户能够查看当前电路中设备的工作状态。

caculateResistance(Map<String, List<String>> Circuits, List<String> parallelCircuits)

  • 功能:计算电路的总电阻,包括串联和并联电路的组合。
  • 实现原理
    • 遍历电路配置,根据电路的串联或并联特性,计算每个电路段的电阻。
    • 使用公式计算并联电阻的倒数和(总电阻为倒数和的倒数)。
    • 更新 resistanceAll 映射,存储每个电路段的计算结果。
  • 关键代码
    for (Map.Entry<String, List<String>> entry : Circuits.entrySet()) {
        List<String> connections = entry.getValue();
        String circuitKey = entry.getKey();
        int seriesResistance = 0;
        for (String conn : connections) {
            String[] parts = conn.split(" ");
            String device1 = parts[0].split("-")[0];
            Device dev1 = devices.get(device1);
            if (dev1 instanceof ControlledDevice) {
                seriesResistance += ((ControlledDevice) dev1).getResistance();
            }
        }
        resistanceAll.put(circuitKey, seriesResistance / 2);
    }
    
  • 作用:为电路中的电流和电压计算提供基础数据,确保模拟电路的物理准确性。

顺序图设计如下:


踩坑心得

问题 1:并联电路电压逻辑设计的难点
  • 初始思路是按分流逻辑设计电路,以为通过计算分流电流可以确定电路的状态,但是实际问题是分流逻辑忽略了并联电路的关键特点,即所有并联分支上的电压是相等的,分流只是结果,不能用于逆推电压。

  • 导致的挑战: 如果直接按照分流逻辑设计,难以获取并联电路的总电压,进而无法准确计算电流和其他设备的状态。

  • 核心点:并联电路的电压是全局共享的,不能依赖分流计算电压。

    • 正确的做法应该先从整个并联电路的等效电阻计算出电流,再根据总电流和等效电阻推导总电压。
  • 忽视设备间的交互关系会导致逻辑复杂化。

    • 每个设备会对电路造成影响,设备状态(如电阻、开关)会动态改变整个电路的行为。
                double current1 = 0.0;
                if (totalparalleResistance - resistance == 0){
                    current1 = current;
                }else {
                    current1 = current * ((double) 1 / (totalparalleResistance)) * (totalparalleResistance - resistance);
                }
                if (device instanceof Switch) {
                    ((Switch) device).setInputCurrent(current1);
                } else if (device instanceof ControlledDevice) {
                    ((ControlledDevice) device).setInputCurrent(current1);
                }
            }
        }
改进方向:
  1. 以“电压统一”为核心设计思路:

    • 计算整个并联电路的等效电阻R。
    • 使用 V = I * R 获取总电压。
    • 在每条支路中直接应用总电压来计算电流和设备状态,避免复杂的逆推逻辑。
  2. 清晰分离电路计算的两步:

    • 第一步:计算电路的等效电阻和总电压。
    • 第二步:基于电压计算各设备的状态(如分支电流)。
    • 这可以有效降低逻辑复杂度,同时避免反复迭代计算。

问题 2: 并联电路中电阻计算的难点
  • 并联电阻的计算公式是:

  • 一旦某条分支的开关断开,其电阻应视为“无穷大”(或直接移除该分支)。

  • 需要动态检测每条分支的状态,并根据开关的状态调整电阻计算。

  • 实际问题:

    • 传统的计算逻辑是静态的,难以处理动态变化(如开关断开或设备状态变化)。
    • 如果未设计灵活的状态管理机制,容易导致重复计算和逻辑错误。
经验教训:
  • 电路中的动态特性必须抽象化处理。
    • 开关、设备状态的变化直接影响整个电路的等效电阻,不能简单地把状态和电阻分离开来。
  • 设备状态对电阻的影响必须明确建模。
    • 如果某条支路断开,需要在计算等效电阻时动态排除这条分支。
private int calculateParallelResistanceForCircuit(List<String> connections) {
    int parallelResistance = 0;
    for (String conn : connections) {
        String[] parts = conn.split(" ");
        if (parts.length < 2) continue;

        String device1 = parts[0].split("-")[0];
        String device2 = parts[1].split("-")[0];

        if (device1.equals("IN") && device2.equals("OUT")) {
            parallelResistance = 0;
            isDuanlu = true;
            break;
        } else if (device1.equals("IN")) {
            Device device22 = devices.get(device2);
            if (device22 instanceof ControlledDevice && device22.nextDevice == null) {
                parallelResistance += ((ControlledDevice) device22).getResistance();
            } else if (device22 instanceof Switch && device22.nextDevice == null && ((Switch) device22).status) {
                parallelResistance = 0;
                isDuanlu = true;
            }
        } else if (device2.equals("OUT")) {
            // No action needed
        } else {
            Device device11 = devices.get(device1);
            Device device22 = devices.get(device2);
            if (device11 instanceof Switch && ((Switch) device11).status) {
                parallelResistance += ((ControlledDevice) device22).getResistance();
            } else if (device22 instanceof Switch && ((Switch) device22).status) {
                parallelResistance += ((ControlledDevice) device11).getResistance();
            }
        }
    }
    return parallelResistance;
}
改进方向:
  1. 动态管理开关状态和电阻的关系:

    • 设计一个“设备状态模型”,统一管理设备的工作状态(如开关是否断开、电阻值是否有效)。
    • 每次计算等效电阻时,自动排除断开的分支。
  2. 引入动态电路模拟:

    • 每个设备提供其当前的电阻值(或无穷大,代表断开)。
    • 并联电路统一收集所有有效分支的电阻,动态计算等效电阻。
  3. 优化电阻计算逻辑:

    • 如果电路状态频繁变化,采用增量更新法,只重新计算受影响的部分电路,而不是每次从头计算整个电路的电阻。

  • 总结

这三次题目集的完成让我收获很多,尤其是在面对复杂问题时的分析和解决能力有了很大的提升。刚开始觉得题目有点挑战,但一步步做下来,发现其实关键在于理清思路,把复杂的逻辑拆分成小模块,再逐一解决。比如在电路题目里,开始总想着直接套公式,但后来慢慢意识到,电路的动态特性需要通过抽象建模去处理,状态管理和逻辑设计比单纯的计算更重要。

这次作业让我真正体会到模块化设计的好处。通过将设备的状态和功能分开,不仅让代码更清晰,也方便调试和扩展。尤其是在电阻、电压、电流计算这些环节,学会了用统一的逻辑去处理动态变化,比最开始那种“写完再改”的混乱方式高效多了。同时,我对递归、动态更新这些算法也有了更深的理解,但也发现自己在算法优化和性能分析上还需要继续加强,尤其是在遇到更复杂的场景时,效率问题会变得很明显。

课程方面,老师的讲解内容其实挺扎实,但有些地方如果能配合更多实际案例,比如把一个复杂的电路设计从头到尾剖析清楚,可能会更有帮助。另外,作业如果能提供一些基础测试用例,就能让我们快速验证自己的逻辑对不对,少花点时间在调试最基本的错误上。

总体来说,这次题目集的设计还是非常有价值的,让我对编程和物理模型结合的能力有了很大提升。但在未来的学习中,我觉得还可以多关注一些图结构和复杂系统的模拟,这部分内容对提升解决问题的深度会更有帮助。最后一点建议是,希望课程里能有更多交流机会,比如课下组队或者案例分享,大家一起讨论其实会碰撞出不少新思路。

posted @ 2024-11-22 16:12  邱瑞韬  阅读(9)  评论(0编辑  收藏  举报