309
社区成员
发帖
与我相关
我的任务
分享该项目旨在解析、求值和化简一个复杂的数学表达式。该表达式支持双变量(x, y)、多层括号、指数函数、自定义函数(非递推与递推)、条件选择,甚至符号求导。
其核心架构思想是组合模式 (Composite Pattern) 和**延迟求值 (Lazy Evaluation)**。
组合模式:
Factor 作为抽象构件接口,定义了所有表达式组件的共同行为 toPoly()。Expr(表达式)、Term(项)是组合节点,它们包含其他 Factor。Number(常数)、Variable(变量)、ExpFunc(指数函数)等是叶节点,它们是表达式的基本单元。Factor。整个复杂的表达式可以被视为一个单一的、顶层的 Factor。延迟求值与规范化:
toPoly() 方法触发。这个方法递归地将整个 AST 转换成一个统一的规范表示——Poly(多项式)。Poly 类是整个设计的核心,它将任何复杂的表达式结构(无论包含多少函数、求导、嵌套)都“压平”成一个标准的“和的标准型”:Σ c_i * x^a_i * y^b_i * exp(P_i)。其中 P_i 本身也是一个 Poly,这优雅地处理了指数嵌套。Poly 这个规范化表示上进行,极大地简化了逻辑,避免了对复杂 AST 的直接操作。下面是项目主要类及其关系的文本表示形式:
========================= 表达式抽象语法树 (AST - Composite Pattern) =========================
[接口] Factor
+ toPoly(xval: Poly, yval: Poly): Poly
^
|____________________________________________________________________________________
| | | | | |
Expr Number Variable ExprFactor ExpFunc SelectFactor
- terms: List<Term> - expr: Expr - factor: Factor - left: Factor
(自身也实现Factor) - exponent - exp: BigInteger - right: Factor
- thenBranch: Factor
- elseBranch: Factor
|
|-----------------------------------------------------------------------------------
| | |
DerivFactor FuncCall IndexedFuncCall
- inner: Factor - arg: Factor - index: int
- op: Op (DX, DY, GRAD) - arg: Factor
* Term (项)
- factors: List<Factor>
(注意: Term 本身不实现 Factor,是 Expr 的一部分)
================================== 解析器 (Parser) =========================================
MainClass --uses--> InputProcessor --uses--> Parser --uses--> Lexer
| | (构建 AST) (生成 Token 流)
| |
| +----------uses---------> FunctionRegistry (存储函数定义)
|
+-----> Poly.fromX() / Poly.fromY() (创建初始代入值)
================================= 规范化表示 (Canonical Form) ===============================
(AST 中所有 Factor) --evaluates to--> Poly
- terms: HashMap<Mono, BigInteger>
+ add, multiply, pow, deriveByX, deriveByY
^
|
| contains
|
Mono
- expX: BigInteger
- expY: BigInteger
- expPoly: Poly <-- 完美的递归定义
=================================== 函数处理 (Function Handling) ==============================
InputProcessor --writes--> FunctionRegistry <--reads-- (FuncCall | IndexedFuncCall)
- simpleFunc: Expr
- recursiveFunc: RecursiveFuncDef
RecursiveFuncDef --evaluates--> Poly
- f0: Expr
- f1: Expr
- g1: Factor, g2: Factor, ...
Factor (接口)
toPoly(xval, yval) 是其核心,xval和yval参数的设计是为了支持函数调用时的变量替换(例如 f(x+1),就是将 x 替换为 x+1 的多项式表示)。Poly 类,这是无法避免的,因为 Poly 是求值的目标形式。Poly (多项式)
exp() 函数的参数,还是求导、恒等性判断等高级操作的执行者。内部使用 HashMap<Mono, BigInteger> 来存储项(Mono)和其系数,天然地支持了同类项合并。Mono。它的 deriveByX/Y 方法体现了链式法则,result.add(new Poly(m, c).multiply(dp)) 这一行代码清晰地展现了其递归求导结构,但同时也造成了对自身的依赖。Mono (单项式)
x^a * y^b * exp(P) 的原子表示。最巧妙的设计是其 expPoly 字段的类型是 Poly,这使得 exp(exp(x)) 这样的嵌套结构得以用递归的方式优雅定义,是整个规范化体系的基石。Poly 对象,形成了 Poly -> Mono -> Poly 的循环依赖,但这对于表达嵌套指数是必要且合理的设计。Expr / Term
Expr 由 Term 相加,Term 由 Factor 相乘。它们通过 toPoly() 方法递归地将计算委托给子节点,并将结果进行累加或累乘。Expr 负责加法,Term 负责乘法。Expr 依赖 Term,Term 依赖 Factor,这是组合模式中典型的、良性的层级耦合。Variable / Number
Poly 形式。ExprFactor / ExpFunc
ExprFactor 处理带括号的表达式 (expr)^exp,ExpFunc 处理指数函数 exp(factor)^exp。它们都是通过递归调用内部因子的 toPoly 方法,然后对返回的 Poly 对象执行 pow 或 exp 相关的构造。Factor 和 Poly。DerivFactor / SelectFactor
Poly 中执行的优势。DerivFactor: 不直接在 AST 上做复杂的求导,而是先把内部表达式 inner 转为 Poly,然后调用 Poly 封装好的 deriveByX/Y 方法。SelectFactor: 同样,先把 A 和 B 转为 Poly,然后通过 pa.add(pb.negate()).isZero() 来判断恒等性。Poly 类。Lexer / Parser
Lexer 负责将字符串变为 token 流,Parser 根据文法消耗 token 并构建 AST。Parser 的方法如 parseExpr, parseTerm, parseFactor 相互递归调用,完美匹配了表达式的递归结构。Lexer 只管分词,Parser 只管语法。Parser 强依赖于 Lexer,这是解析器设计的标准模式,耦合是必然且合理的。InputProcessor
FunctionRegistry 来注册函数,解耦了函数定义和函数调用的过程。Scanner, Parser, Lexer, FunctionRegistry 等多个类交互。FunctionRegistry / RecursiveFuncDef / FuncCall / IndexedFuncCall
FunctionRegistry: 使用静态类作为单例注册表,提供全局的函数定义访问点,实现了“定义”与“使用”的解耦。FuncCall / IndexedFuncCall: 在 toPoly 方法中,从 FunctionRegistry 获取函数定义,然后执行替换求值。RecursiveFuncDef: 将递推函数的复杂逻辑(基例、递推式、参数)封装在一个对象中,evaluate 方法清晰地实现了递归展开。FuncCall 和 IndexedFuncCall 依赖 FunctionRegistry,这是典型的“服务定位器”模式带来的耦合,对于此场景是合适的。架构清晰,易于扩展:基于组合模式和规范化表示 (Poly) 的架构非常成功。当需要添加新的因子(例如 sin(x))时,只需:
SinFactor 类实现 Factor 接口。Parser 中添加相应的解析逻辑。Poly 和 Mono 中添加对 sin 的表示和求导/运算法则。**关注点分离 (SoC)**:
Parser 和 Lexer 专注于构建 AST,而 toPoly 方法和 Poly 类专注于计算。Poly 类中,而不是散落在各个 AST 节点的类里。这使得逻辑的实现和维护更加简单。FunctionRegistry 作为中间人,让解析器可以先定义函数,而调用处无需知道函数来自何处。 Parser 类过于庞大:Parser 类承担了所有因子的解析任务,包括非递推/递推函数的特殊解析。虽然功能内聚,但代码量较大,可以考虑将某些复杂的解析逻辑(如 parseRecurrenceDef)提取到辅助类中,使 Parser 更专注于通用的表达式解析。
静态 FunctionRegistry 的限制:虽然 FunctionRegistry 实现了很好的解耦,但使用 static 字段使其成为一个全局单例。这在多线程环境下会产生问题(虽然本次作业是单线程),也不利于单元测试(测试之间会相互干扰,需要手动 reset())。可以考虑使用依赖注入的方式,将 FunctionRegistry 的实例传递给需要它的地方(如 Parser, FuncCall),从而避免全局状态。
性能优化:Poly 的 pow 方法和 RecursiveFuncDef 的 evaluate 方法都使用了递归/迭代,对于大指数或深的递归层级,可能会有性能问题。可以引入记忆化 (Memoization) 来缓存计算结果,例如在 RecursiveFuncDef.evaluate 中用一个 Map<Integer, Poly> 缓存 f{n}(arg) 的结果,避免重复计算。
| 维度 | 第一次作业 (hw0) | 第二次作业 (hw2) | 第三次作业 (hw2_test2) |
|---|---|---|---|
| 变量支持 | 单变量 x | 单变量 x | 双变量 x, y |
| 因子类型 | Number, Variable, ExprFactor | +ExpFunc, FuncCall, SelectFactor | +DerivFactor, IndexedFuncCall |
| 多项式表示 | TreeMap<BigInteger, BigInteger> | HashMap<Mono, BigInteger> | HashMap<Mono, BigInteger>(Mono 扩展为二元) |
| 函数支持 | 无 | 简单自定义函数 f(x) | 简单函数 + 递归函数 f{n}(x) |
| 求导支持 | 无 | 无 | dx, dy, grad 符号求导 |
| 条件表达式 | 无 | [A==B?C:D] | [A==B?C:D] |
| 核心类数量 | 10 | 15 | 19 |
第一次作业构建了经典的三层架构,为后续迭代奠定了坚实的基础:
输入字符串 → Lexer(词法分析)→ Parser(语法分析)→ 表达式树 → toPoly()求值 → Poly → toString()输出
核心设计决策:
Composite 模式的引入:定义 Factor 接口作为所有因子的统一抽象,Number、Variable、ExprFactor 均实现该接口。Expr 包含 List<Term>,Term 包含 List<Factor>,形成递归的树形结构。
递归下降解析器:Parser 通过 parseExpr() → parseTerm() → parseFactor() 的方法调用层次直接映射文法的优先级规则,结构清晰。
多项式统一表示:所有因子通过 toPoly() 方法转化为 Poly 对象,利用 TreeMap<BigInteger, BigInteger>(指数 → 系数)存储一元多项式,支持加法、乘法、快速幂运算。
类层次结构:
Factor (接口)
├── Number (常数因子)
├── Variable (变量 x^n)
└── ExprFactor (括号表达式因子 (expr)^n)
Expr → 包含 List<Term>
Term → 包含 List<Factor>
Poly → TreeMap<BigInteger, BigInteger> // 指数 → 系数
此阶段架构简洁,职责分离清晰:Lexer 负责词法、Parser 负责语法、Factor 子类负责语义求值、Poly 负责多项式运算和输出格式化。
第二次作业是架构变化最大的一次迭代,引入了指数函数 exp()、自定义函数 f(x) 和条件表达式 [A==B?C:D],这些新需求直接驱动了多项式表示层的重构。
重构原因: 第一次作业的 Poly 使用 TreeMap<BigInteger, BigInteger> 仅能表示形如 $\sum c_i x^{e_i}$ 的纯一元多项式。引入 exp() 后,表达式变为形如 $c \cdot x^a \cdot \exp(P(x))$ 的形式,旧的数据结构根本无法表达。
重构方案: 引入 Mono(单项式)类,将 Poly 的表示从 TreeMap<BigInteger, BigInteger> 重构为 HashMap<Mono, BigInteger>。
| 对比项 | 重构前 (hw0) | 重构后 (hw2) |
|---|---|---|
| 多项式键 | BigInteger(x 的指数) | Mono(含 expX 和 expPoly) |
| 多项式值 | BigInteger(系数) | BigInteger(系数) |
| 单项式含义 | $c \cdot x^a$ | $c \cdot x^a \cdot \exp(P(x))$ |
| 容器类型 | TreeMap(有序) | HashMap(需自定义 equals/hashCode) |
| 合并同类项 | 指数相同即合并 | Mono 的 expX 和 expPoly 均相同才合并 |
Mono 类的设计:
class Mono implements Comparable<Mono> {
BigInteger expX; // x 的指数
Poly expPoly; // exp() 内的多项式(可为空表示无 exp)
// 单项式乘法:指数相加,exp 内多项式相加
// (x^a * exp(P)) * (x^b * exp(Q)) = x^(a+b) * exp(P+Q)
Mono multiply(Mono other);
}
这一重构是不可避免的结构性改变——旧的键类型(单个 BigInteger)在语义上不足以表达新的单项式结构。
| 新增类 | 作用 | 实现方式 |
|---|---|---|
ExpFunc | 指数函数 exp(factor)^n | 利用 exp 幂律:$\exp(a)^n = \exp(n \cdot a)$ |
FuncCall | 自定义函数调用 f(arg) | 将参数求值后代入函数体(存储在 MainClass 静态字段) |
SelectFactor | 条件表达式 [A==B?C:D] | 判断 A-B 是否为零多项式 |
toPoly() 接口的变化// hw0: 无参
Poly toPoly();
// hw2: 传入 x 的实际多项式值(用于函数调用时的变量替换)
Poly toPoly(Poly xval);
这一变化使得函数调用的实参替换成为可能:调用 f(x+1) 时,将 xval = Poly(x+1) 传入函数体的 toPoly(xval),自然完成变量替换。
这次重构的核心挑战在于 Poly 类几乎需要完全重写——数据结构从简单映射变为复杂的 Mono-Coefficient 映射,加法、乘法、toString() 等所有方法都需要适配新结构。但由于第一次作业建立了清晰的分层架构(Lexer → Parser → Factor → Poly),重构的影响被有效隔离在 Poly 层和 Factor.toPoly() 接口上,Parser 和 Lexer 几乎不受影响(仅需扩展新的 token 识别和 parseFactor 分支)。
这验证了良好的分层架构在面对需求变化时的抗风险能力:底层表示的剧变并未导致整个系统的崩塌。
第三次作业的变化幅度明显小于第二次,主要是在已有架构上进行增量扩展,体现了重构后架构的良好可扩展性。
// hw2 Mono
Mono(BigInteger expX, Poly expPoly)
// hw2_test2 Mono
Mono(BigInteger expX, BigInteger expY, Poly expPoly)
仅增加一个字段 expY,表示 $c \cdot x^a \cdot y^b \cdot \exp(P(x,y))$。这是对已有 Mono 结构的自然延伸,乘法规则同样自然扩展:指数分别相加。
// hw0/hw2: Variable 仅表示 x
Variable(BigInteger exponent)
// hw2_test2: Variable 区分 x 和 y
Variable(String name, BigInteger exponent) // name = "x" 或 "y"
toPoly() 接口再次扩展// hw2
Poly toPoly(Poly xval);
// hw2_test2
Poly toPoly(Poly xval, Poly yval);
所有 Factor 子类的 toPoly 签名统一变更,Variable 根据 name 选择返回 xval.pow(exp) 或 yval.pow(exp)。
| 新增类 | 作用 |
|---|---|
DerivFactor | 符号求导:dx(对x偏导)、dy(对y偏导)、grad(梯度) |
IndexedFuncCall | 递归函数调用 f{k}(arg) |
RecursiveFuncDef | 递归函数定义:$f{n} = c_1 f{n-1}(g_1) + c_2 f{n-2}(g_2) + \text{extra}$ |
FunctionRegistry | 函数注册中心,取代 hw2 中 MainClass 的静态字段 |
Poly deriveByX(); // ∂P/∂x
Poly deriveByY(); // ∂P/∂y
对每个项 $c \cdot x^a \cdot y^b \cdot \exp(Q)$ 应用求导规则:
这是在 Poly 类上的增量新增,不影响已有的加法、乘法等运算。
第二次作业中,函数体直接存储在 MainClass.funcBody 静态字段中,FuncCall 直接引用 MainClass.getFuncBody()。第三次作业将其抽取为独立的 FunctionRegistry 类,管理简单函数和递归函数,降低了耦合。
三次作业中,第一次到第二次之间发生了一次重大重构,核心变化集中在多项式表示层:
hw0 Poly: TreeMap<BigInteger, BigInteger>
↓ 重构(引入 Mono 类)
hw2 Poly: HashMap<Mono, BigInteger>
↓ 平稳扩展(Mono 增加 expY)
hw3 Poly: HashMap<Mono, BigInteger> // Mono(expX, expY, expPoly)
hw0 → hw2 重构影响
─────────────────────────────────────────
Lexer (新增 token 类型)
Parser (新增 parse 分支)
Factor 接口 (toPoly 增加参数)
各 Factor 类 (适配新 toPoly 签名)
Poly (数据结构根本性变更)
Mono (全新类)
─────────────────────────────────────────
hw2 → hw3 扩展影响
─────────────────────────────────────────
Lexer (新增 token 类型)
Parser (新增 parse 分支)
Factor 接口 (toPoly 增加 yval)
Mono (增加 expY 字段)
Poly (新增求导方法)
新增类 DerivFactor 等 4 个新类
─────────────────────────────────────────
第一次重构(hw0→hw2)的痛点:Poly 的数据结构是整个系统的"地基",改动它意味着所有依赖 Poly 的代码(即几乎所有 Factor 类的 toPoly 实现、toString 输出逻辑)都需要联动修改。但因为 Factor 接口的统一抽象,新增的 Factor 类型(ExpFunc、FuncCall、SelectFactor)可以自然地融入现有框架——只需实现 toPoly() 即可。
第二次扩展(hw2→hw3)的顺畅:得益于重构后 Mono+Poly 的灵活表示,引入第二个变量 y 只需在 Mono 中新增一个字段,求导功能也只是在 Poly 上新增方法。新的 Factor 类型(DerivFactor、IndexedFuncCall)同样只需实现接口即可接入系统。架构已经"成型",扩展变得轻松。
第一次作业 (hw0) 第二次作业 (hw2) 第三次作业 (hw2_test2)
───────────────── ───────────────── ─────────────────────
MainClass MainClass MainClass
│ │ │
Parser ← Lexer InputProcessor InputProcessor
│ │ │ │ │
parseExpr/Term/Factor Parser ← Lexer Parser ← Lexer
│ │ │
┌─────┴─────┐ ┌──────┴──────┐ ┌──────┴──────────┐
Expr Term Expr Term Expr Term
│ │ │ │ │ │
Factor(接口) Factor(接口) Factor(接口)
├─ Number ├─ Number ├─ Number
├─ Variable ├─ Variable ├─ Variable(x/y)
├─ ExprFactor ├─ ExprFactor ├─ ExprFactor
│ ├─ ExpFunc 新增 ├─ ExpFunc
│ ├─ FuncCall 新增 ├─ FuncCall
│ ├─ SelectFactor 新增 ├─ SelectFactor
│ │ ├─ DerivFactor 新增
│ │ ├─ IndexedFuncCall 新增
│ │ │
Poly Poly ← Mono 重构 Poly ← Mono(含y) 扩展
(TreeMap) (HashMap<Mono,BI>) (+deriveByX/Y)
FunctionRegistry 新增
RecursiveFuncDef 新增
假设第四次作业要求新增 sin(factor) 和 cos(factor) 的支持,表达式可包含形如 $c \cdot x^a \cdot y^b \cdot \sin(P)^m \cdot \cos(Q)^n \cdot \exp(R)$ 的项。
1. Mono 层扩展
class Mono {
BigInteger expX, expY;
Poly expPoly; // exp() 内多项式(已有)
// 新增:
Map<Poly, BigInteger> sinTerms; // sin(P)^m
Map<Poly, BigInteger> cosTerms; // cos(Q)^n
}
Mono 的 multiply()、equals()、hashCode()、compareTo() 需相应扩展,但这是局部改动。
2. 新增 Factor 子类
class SinFunc implements Factor {
Factor inner;
BigInteger exponent;
Poly toPoly(Poly xval, Poly yval) { ... }
}
class CosFunc implements Factor { /* 类似 */ }
这与 ExpFunc 的实现方式完全类似,只需实现 Factor 接口即可无缝接入。
3. Parser 扩展
在 parseFactor() 中新增 "sin" 和 "cos" 的分支,逻辑与 parseExpFunc() 类似。Lexer 新增 "sin"、"cos" token 识别。
4. Poly 求导扩展
需在 deriveByX()/deriveByY() 中增加三角函数的链式法则。
| 评估维度 | 结论 |
|---|---|
| 新增类数量 | 2 个 Factor 子类(SinFunc、CosFunc) |
| 已有类修改 | Mono(增加字段)、Poly(求导规则)、Parser/Lexer(新语法) |
| 架构变动 | 无需改变架构,所有改动都是在已有框架内的增量扩展 |
| 风险等级 | 低——Mono 扩展虽涉及 equals/hashCode,但模式与 expPoly 完全一致 |
当前架构的 Factor 接口 + Mono/Poly 表示 组合提供了良好的扩展点:新的数学函数只需实现 Factor 接口并在 Mono 中增加对应的指数字段即可。这正是 Composite 模式和统一求值接口 toPoly() 带来的设计红利。
三次作业的架构演进路径清晰:
exp() 函数,对 Poly 层进行了结构性重构(引入 Mono),同时扩展了 Factor 接口的参数签名以支持变量替换。整体来看,第一次作业的 分层设计 和 Factor 接口抽象 是架构长期健壮的关键;第二次作业的 Mono 重构 虽然痛苦但为后续扩展打下了良好基础;第三次作业的平稳扩展则验证了这一架构决策的正确性。
代码一般通过借助AI给出的数据点来进行测试,主要通过数据大小以及边界情况来进行分析和针对性测试,对于互测中被hack到的数据点,进行针对性的模块和对应路径分析,得益于代码的架构和职责单一使得bug在某一条链路中较为容易找到。
没有使用优化,以至于性能分一直以来都较差。
最深刻的是在研讨课上与老师和组员们的交流产生的思想碰撞和感受。
归一化的处理思想体现在预处理中的时候,我触碰到了自己思维的盲区,让每一个模块在分工明确中,各自处理好自己的事情,而不是全部包揽给某一个模块。
同样是使用大模型,不同的人有不同的用法,有的人用大模型来解放自己的大脑,有的人用大模型来解放自己的双手。
更多的注重思路和想法的展现,而不是细节的实现吧。