Python回溯算法实战:从算24点游戏深入理解递归与表达式树

📅 2026/8/14 10:24:01
Python回溯算法实战:从算24点游戏深入理解递归与表达式树
1. 项目概述从游戏到算法的思维跃迁算24点这个几乎人人都玩过的扑克牌游戏规则简单到一句话就能说清从一副扑克中随机抽取四张牌使用加、减、乘、除以及括号将这四张牌对应的数字A为1J、Q、K分别为11、12、13计算出24。但就是这么个简单的游戏背后却隐藏着丰富的数学逻辑和算法思想。我最初接触这个项目并非为了怀旧而是在思考如何将一个具体的、有明确规则的问题转化为计算机可以理解和求解的模型。这恰恰是编程思维的核心——将现实问题抽象化、逻辑化、步骤化。用Python对算24游戏进行全面分析就是一个绝佳的练手项目它不仅能巩固你的Python基础语法、数据结构列表、栈、递归更能深入理解回溯算法、表达式树的构建与遍历甚至能延伸到概率统计和游戏策略分析。无论你是刚学完Python基础语法的新手想找一个综合性项目练手还是有一定经验的开发者希望深入理解算法设计与性能优化这个项目都能给你带来实实在在的收获。接下来我将带你从零开始一步步拆解这个游戏的算法核心并实现一个功能完备的分析工具。2. 核心算法设计与思路拆解2.1 问题本质与数学模型建立算24点的核心是寻找一个合法的算术表达式。这个表达式由四个操作数和三个运算符 - * /以及可能的括号构成其计算结果等于24。这里的“合法”有几层含义首先运算必须符合数学优先级乘除优先于加减括号可以改变优先级其次除法运算必须是有意义的即除数不能为零且通常我们约定为精确的实数除法在Python中即浮点数除法而非整数除法。那么如何让计算机来“思考”这个问题呢最直观的想法是穷举。我们需要穷举所有可能的数字排列顺序四个数字所有可能的排列。例如数字[1, 2, 3, 4][1, 2, 4, 3]就是两种不同的排列因为操作数的顺序会影响运算结果特别是减法和除法。运算符组合三个位置每个位置可以是4种运算符 - * /之一因此有4^364种组合。运算顺序即括号的添加方式这是最复杂的一部分。对于四个数和三个运算符其运算顺序即表达式树的结构是有限的。本质上这就是二叉树的不同形态。通过组合以上三者我们就能生成所有可能的表达式然后逐个计算其值判断是否等于24考虑到浮点数精度通常判断是否与24的差值在一个极小的范围内如1e-6。2.2 算法选型为什么是递归与回溯面对这种需要尝试所有可能组合的问题递归回溯是天然的解决方案。我们可以将问题分解先从四个数中任选两个数用四种运算符连接得到一个结果。这个结果和剩下的两个数就构成了一个新的“三个数求24”的子问题。然后对子问题重复这个过程直到只剩一个数判断这个数是否为24。这个“选两个数合并成一个新数”的过程完美契合了递归的“分而治之”思想。而回溯则体现在当某条计算路径走不通时例如所有运算符都试过了也无法得到目标值我们需要撤销上一步的选择尝试其他可能性。另一种更接近表达式生成的思路是直接构造所有可能的表达式树。对于N个操作数其所有可能的二叉树形态是卡特兰数。对于4个数有5种不同的基本二叉树结构对应5种括号添加方式。我们可以先生成所有数字排列再为每种排列填充所有运算符组合最后套用这5种二叉树结构进行计算。这种方法逻辑更清晰更利于后续的表达式输出。注意在实际编码中递归回溯法在代码实现上相对简洁而表达式树法则在输出具体算式时更有优势。本文将重点讲解更通用、易于理解的递归回溯法并在后续扩展中提及表达式树思路。2.3 工具选型Python内置数据结构的妙用我们几乎不需要任何第三方库。核心将依赖列表List存储当前的数字集合用于递归传递和修改。递归函数实现核心的回溯算法。itertools.permutations高效生成数字的所有排列避免自己写复杂的排列逻辑。math.isclose用于安全地比较浮点数是否相等避免0.1 0.2 ! 0.3这类精度问题。选择纯内置库实现能让项目保持轻量且其逻辑适用于任何Python环境便于理解和传播。3. 核心细节解析与实操要点3.1 浮点数精度处理第一个“坑”这是本项目遇到的第一个也是最重要的一个技术细节。在Python中1 / 3 * 3的结果并不是精确的1.0而是一个极其接近1.0的浮点数。如果我们直接用来判断计算结果是否等于24绝大多数情况都会失败。解决方案使用math.isclose(a, b)函数或者自定义一个容差范围。import math def is_equal_24(value): # 方法一使用math.isclose rel_tol是相对容差abs_tol是绝对容差 return math.isclose(value, 24.0, rel_tol1e-6) # 方法二自定义绝对值容差 # return abs(value - 24.0) 1e-6实操心得rel_tol相对容差通常比abs_tol绝对容差更合理。例如对于24这个量级1e-6的相对容差意味着允许约±0.000024的误差这完全足够。如果只用绝对容差对于计算结果可能很大或很小的其他变种游戏如算30、算1就需要调整容差值。3.2 除法运算的合法性校验在尝试除法运算时必须检查除数是否为零。但这里还有一个隐含条件在我们的游戏中通常要求每一步运算都是“有意义的”即中间结果也应该是精确的在浮点数误差内。然而更严格的玩法是要求每一步都是整数除法且能整除。为了通用性我们通常只做零除检查但如果你要模拟某些特定规则可能需要记录运算过程并校验。def operate(a, b, op): 执行运算如果非法如除零则返回None if op : return a b elif op -: return a - b elif op *: return a * b elif op /: if b 0: # 简单的零除检查更精确可用math.isclose(b, 0) return None return a / b else: return None3.3 递归函数的设计与状态管理递归函数是算法的引擎。它的输入是当前的一个数字列表nums输出是布尔值表示能否算出24同时为了能最终输出表达式我们通常还需要一个额外的参数来记录运算步骤。一个简洁的递归函数设计如下def solve_24(nums, historyNone): :param nums: 当前数字列表元素为float :param history: 运算历史记录用于回溯生成算式 :return: (bool, str) 能否算出24以及对应的算式 if history is None: history [] if len(nums) 1: return (is_equal_24(nums[0]), history[0] if history else str(nums[0])) # 从nums中任选两个不同的索引i, j # 任选一种运算符op # 计算新数 new_num operate(nums[i], nums[j], op) # 如果new_num是None如除零跳过 # 构造新的数字列表 new_nums [new_num] [nums[k] for k in range(len(nums)) if k not in (i, j)] # 构造新的历史记录 new_history ... # 递归调用 solve_24(new_nums, new_history) # 如果递归返回True则当前路径成功层层返回 # 如果所有i, j, op组合都失败则返回False关键点history记录的是如何从原始四个数得到当前nums中每个数的表达式字符串。当合并两个数时需要生成一个新的表达式字符串通常是(A op B)其中A和B是它们各自的表达式。这样在递归到底层时得到的表达式就是完整的。注意这种记录方式在遇到除法时即使b不是零但a/b可能因为浮点精度导致后续判断失败。一种更健壮的做法是history不仅记录表达式字符串也记录该表达式的精确值分数形式或高精度小数但这会大大增加复杂度。对于大多数分析目的浮点数精度加isclose判断已经足够。4. 实操过程与核心环节实现4.1 基础递归回溯算法实现下面是一个实现了上述思路的完整函数。它返回第一个找到的解如果存在。import itertools import math def find_24_solution(numbers): 给定一个包含4个数字的列表返回一个能计算出24的算式字符串如果无解则返回None。 使用递归回溯法。 ops [, -, *, /] def dfs(nums, exprs): 深度优先搜索 n len(nums) if n 1: if math.isclose(nums[0], 24, rel_tol1e-6): return exprs[0] # 返回完整的表达式 return None # 遍历所有选择两个不同索引的组合 for i, j in itertools.combinations(range(n), 2): # 剩下的数字索引 rest_indices [k for k in range(n) if k not in (i, j)] for op in ops: # 尝试计算 nums[i] op nums[j] if op / and math.isclose(nums[j], 0, rel_tol1e-10): continue # 跳过除零 if op : new_val nums[i] nums[j] elif op -: new_val nums[i] - nums[j] elif op *: new_val nums[i] * nums[j] else: # / new_val nums[i] / nums[j] # 构造新的表达式。注意减法和除法顺序敏感有两种可能(a-b)和(b-a) # 我们这里固定为 (exprs[i] op exprs[j])但为了完整性应该考虑两种顺序。 # 更严谨的做法是对减法和除法分别尝试 i-j 和 j-i。 # 以下简化版只处理了 i op j 的顺序。 new_expr f({exprs[i]} {op} {exprs[j]}) # 构建新的数字和表达式列表 new_nums [new_val] [nums[k] for k in rest_indices] new_exprs [new_expr] [exprs[k] for k in rest_indices] # 递归搜索 res dfs(new_nums, new_exprs) if res is not None: return res # 回溯对于减法和除法尝试另一种顺序 (j op i) if op - or op /: if op -: new_val_rev nums[j] - nums[i] new_expr_rev f({exprs[j]} {op} {exprs[i]}) else: # / if math.isclose(nums[i], 0, rel_tol1e-10): continue new_val_rev nums[j] / nums[i] new_expr_rev f({exprs[j]} {op} {exprs[i]}) new_nums_rev [new_val_rev] [nums[k] for k in rest_indices] new_exprs_rev [new_expr_rev] [exprs[k] for k in rest_indices] res_rev dfs(new_nums_rev, new_exprs_rev) if res_rev is not None: return res_rev return None # 初始表达式就是数字本身转为字符串 init_exprs [str(num) for num in numbers] return dfs(numbers, init_exprs) # 测试 test_cases [ [1, 2, 3, 4], # 有解 [1, 1, 1, 1], # 无解 [3, 3, 8, 8], # 经典难题8 / (3 - 8/3) [5, 5, 5, 1], # 有解 ] for case in test_cases: solution find_24_solution(case) print(f数字 {case}: {solution if solution else 无解})代码解读dfs是核心递归函数nums是当前数值列表exprs是对应的表达式字符串列表。使用itertools.combinations选择两个不同的索引i, j确保不重复选取。遍历四种运算符。对于减法和除法由于顺序影响结果我们分别尝试了a-b和b-a或a/b和b/a。这是保证算法完备性的关键。构造新的数值和表达式列表时将运算结果放在列表首位方便处理。递归调用dfs。如果找到解就通过返回值层层传递上来。主函数find_24_solution负责初始化并启动递归。4.2 算法优化引入排列与缓存上面的基础版本存在重复计算。例如对于数字[1,2,3,4]先算123得到[3,3,4]和先算347得到[1,2,7]是两条不同的路径但它们在后续递归中可能会遇到相同的子问题如[3,7]。此外数字的初始顺序也被固定了虽然我们的combinations遍历了所有两两组合但数字的原始排列也会影响表达式结构。一个优化方向是在递归入口先对nums进行排序并转为元组以此作为键将计算结果能否算出24缓存起来。这就是记忆化搜索Memoization能大幅剪枝。from functools import lru_cache def solve_24_optimized(numbers): ops [, -, *, /] lru_cache(maxsizeNone) def can_solve(nums_tuple): nums_tuple 是排序后的数字元组返回是否能算出24 nums list(nums_tuple) n len(nums) if n 1: return math.isclose(nums[0], 24, rel_tol1e-6) # 生成所有两两组合 for i in range(n): for j in range(i1, n): a, b nums[i], nums[j] rest [nums[k] for k in range(n) if k not in (i, j)] for op in ops: # 尝试 a op b results [] if op : results.append(a b) elif op -: results.append(a - b) results.append(b - a) # 减法有两种可能 elif op *: results.append(a * b) elif op /: if not math.isclose(b, 0, rel_tol1e-10): results.append(a / b) if not math.isclose(a, 0, rel_tol1e-10): results.append(b / a) for new_val in results: new_nums sorted(rest [new_val]) # 排序以便缓存 if can_solve(tuple(new_nums)): return True return False # 初始时对数字排序并转为元组 return can_solve(tuple(sorted(numbers)))优化点lru_cache自动为我们缓存函数结果输入是排序后的数字元组。相同的数字集合无论顺序如何都会命中缓存。在递归内部生成新数字列表后立即排序保证缓存键的一致性。这个函数只返回True/False不返回具体表达式速度更快适合用于大规模统计分析。4.3 扩展找出所有解并输出表达式有时我们想知道有多少种解法。这就需要修改算法让它收集所有成功的路径而不是找到第一个就返回。同时为了输出表达式我们需要在递归过程中传递并记录运算步骤。def find_all_24_solutions(numbers): 找出所有可能的算式返回一个列表 ops [, -, *, /] all_solutions [] def dfs(nums, exprs, steps): n len(nums) if n 1: if math.isclose(nums[0], 24, rel_tol1e-6): # 去掉最外层的括号使表达式更简洁 final_expr exprs[0] if final_expr.startswith(() and final_expr.endswith()): # 简单判断可能不绝对准确更严谨需解析表达式树 final_expr final_expr[1:-1] all_solutions.append((final_expr, steps.copy())) return for i in range(n): for j in range(i1, n): a, b nums[i], nums[j] expr_a, expr_b exprs[i], exprs[j] rest_nums [nums[k] for k in range(n) if k not in (i, j)] rest_exprs [exprs[k] for k in range(n) if k not in (i, j)] for op in ops: # 处理加法和乘法满足交换律可避免完全重复 if op in (, *) and i j: # 对于交换律运算规定索引小的在前避免生成如 (ba) 和 (ab) 这种实质相同的表达式 # 这里简单跳过更精细的去重可以在最后对表达式字符串做规范化处理 continue new_vals [] new_exprs_list [] step_infos [] if op : new_vals.append(a b) new_exprs_list.append(f({expr_a} {expr_b})) step_infos.append(f{a} {b} {ab}) elif op -: new_vals.append(a - b) new_exprs_list.append(f({expr_a} - {expr_b})) step_infos.append(f{a} - {b} {a-b}) new_vals.append(b - a) new_exprs_list.append(f({expr_b} - {expr_a})) step_infos.append(f{b} - {a} {b-a}) elif op *: new_vals.append(a * b) new_exprs_list.append(f({expr_a} * {expr_b})) step_infos.append(f{a} * {b} {a*b}) elif op /: if not math.isclose(b, 0, rel_tol1e-10): new_vals.append(a / b) new_exprs_list.append(f({expr_a} / {expr_b})) step_infos.append(f{a} / {b} {a/b}) if not math.isclose(a, 0, rel_tol1e-10): new_vals.append(b / a) new_exprs_list.append(f({expr_b} / {expr_a})) step_infos.append(f{b} / {a} {b/a}) for idx in range(len(new_vals)): new_num new_vals[idx] new_expr new_exprs_list[idx] step step_infos[idx] dfs([new_num] rest_nums, [new_expr] rest_exprs, steps [step]) init_exprs [str(num) for num in numbers] dfs(numbers, init_exprs, []) return all_solutions这个版本会搜索所有分支并将找到的解法存入all_solutions列表。它同时记录了运算步骤steps。需要注意的是这样会找到大量本质相同但形式不同的解例如(ab)c和a(bc)虽然计算结果和表达式树不同但结合律导致它们看起来“一样”。在实际分析中我们可能需要对解进行去重这可以通过对表达式字符串进行规范化例如利用二叉树的中序遍历生成标准形式来实现复杂度较高。5. 常见问题与排查技巧实录在实现和运行算24点程序的过程中你肯定会遇到一些典型问题。下面是我踩过的一些坑和解决方法。5.1 问题一程序运行特别慢或者对于某些输入陷入死循环排查思路检查递归终止条件确保len(nums) 1时一定会返回并且没有遗漏的情况。检查除零处理浮点数判断除零要用math.isclose(b, 0)直接b 0可能因为精度问题漏判导致产生inf或nan这些值参与后续运算会出问题。检查数字列表的更新在创建new_nums时确保正确排除了已使用的两个数i, j并将新数加入。索引处理错误会导致列表长度不对或包含错误元素。无限递归与重复状态如果没做优化算法会尝试大量重复的路径。例如数字[a, b, c, d]先算ab再算cd和先算cd再算ab最终得到的数字集合都是[ab, cd]但程序会当作两条路径都走一遍。这就是引入缓存记忆化优化的原因。解决方案实现上面提到的solve_24_optimized函数使用lru_cache对子问题的结果进行缓存。对于4个数的问题即使不优化也能瞬间完成但如果你要分析所有扑克牌组合1820种优化能带来百倍的速度提升。5.2 问题二找到了解但表达式里有很多不必要的括号看起来不美观。原因我们的算法在每一步合并时都加上了括号(A op B)这是最保守且正确的做法因为它保证了运算顺序与计算过程完全一致。但有些括号在数学优先级下是多余的比如(ab)c。解决方案实现一个表达式简化函数。这需要定义运算符优先级并在生成表达式时根据子表达式的运算符和当前运算符的优先级决定是否添加括号。这是一个中等难度的语法树问题。一个简单的启发式规则是如果当前运算符是或-且子表达式是*或/则子表达式需要括号。如果当前运算符是*或/且子表达式是或-则子表达式需要括号。其他情况可以省略括号。更健壮的做法是构建表达式树然后通过中序遍历生成字符串在遍历时根据优先级规则动态添加括号。5.3 问题三如何验证我的算法找出的解是正确的手动验证最简单的是用Python的eval()函数。但要注意eval执行字符串代码有安全风险绝对不要对来自不可信来源的字符串使用eval。在我们的场景下表达式是自己生成的可以谨慎使用。def verify_solution(expr, numbers): 验证表达式expr使用给定的numbers是否能算出24 try: # 注意这里假设expr中的数字就是numbers中的数字。 # 更严格的验证需要检查expr是否恰好使用了这四个数各一次。 result eval(expr) return math.isclose(result, 24, rel_tol1e-6) except ZeroDivisionError: return False自动化测试编写一个测试套件包含有解和无解的经典案例用程序计算并断言结果是否符合预期。def test_solver(): solver find_24_solution # 或你的求解函数 assert solver([1,2,3,4]) is not None assert solver([1,1,1,1]) is None assert solver([3,3,8,8]) is not None # 验证解的正确性 solution solver([5,5,5,1]) if solution: assert verify_solution(solution, [5,5,5,1]) print(所有测试通过)5.4 问题四我想分析所有四张扑克牌的组合统计有解的比例该怎么做这是本项目一个很自然的扩展。一副牌去掉大小王有52张。从中任选4张组合数为C(52,4)270725。但点数只有1到13很多组合是重复的如四张不同的A。我们通常只关心数字组合不考虑花色。数字组合数是从1~13中可重复地选取4个数因为花色不同但数字相同的牌算多张这等价于多重集的组合计算稍微复杂。更简单的方法是遍历所有可能的4个数字每个1~13并考虑每个数字出现的次数不超过4因为一副牌里每个点数有4张。一个实用的方法是生成所有非递减的四元组(a,b,c,d)其中1abcd13。这样的组合数可以计算也可以用itertools.combinations_with_replacement生成。import itertools def analyze_all_combinations(): 分析所有数字组合的可解性 numbers_range range(1, 14) # 1到13 total 0 solvable 0 solutions_dict {} # 生成所有非递减的四元组组合考虑重复 for combo in itertools.combinations_with_replacement(numbers_range, 4): total 1 # 但combo是排序的例如(1,1,2,3)。实际抽牌时数字顺序是任意的。 # 我们的求解函数接受列表内部会处理顺序。所以直接传入list(combo)即可。 # 注意由于组合中数字可能重复其排列数少于24种但我们的算法通过两两组合已涵盖所有运算顺序。 if find_24_solution(list(combo)): solvable 1 solutions_dict[combo] find_24_solution(list(combo)) # 进度提示 if total % 500 0: print(f已处理 {total} 种组合...) print(f\n总计分析 {total} 种数字组合。) print(f有解的组合数: {solvable}) print(f可解比例: {solvable/total:.2%}) # 可以进一步分析哪些组合无解 return solvable/total, solutions_dict # 运行分析可能需要一些时间优化后很快 # solve_rate, sol_dict analyze_all_combinations()运行这个分析你会得到一个经典结论大约80%的随机四张牌组合是有解的。这个比例因具体规则是否允许分数除法、是否允许乘方等略有浮动。5.5 性能瓶颈与优化记录当你试图找出所有解或者分析所有组合时性能成为关键。以下是我实践中总结的优化点使用缓存记忆化如前所述这是最大的性能提升点。将子问题数字集合的结果缓存起来避免重复计算。尽早剪枝在递归中如果当前数字集合的最大可能值小于24或最小可能值大于24考虑加减乘除后估算可以提前返回False。但估算本身有计算成本需要权衡。利用对称性加法和乘法满足交换律。在遍历i, j时如果op是或*可以强制要求i j避免生成ab和ba这种等价路径。使用内置函数和高效数据结构itertools.combinations比手动写循环快。使用元组而非列表作为缓存键。并行计算分析所有组合时每个组合是独立的可以很容易地用multiprocessing库进行并行处理充分利用多核CPU。一个简单的并行化示例from multiprocessing import Pool def check_combo(combo): return combo, find_24_solution(list(combo)) is not None def parallel_analysis(): combos list(itertools.combinations_with_replacement(range(1,14), 4)) with Pool(processes4) as pool: # 使用4个进程 results pool.map(check_combo, combos) solvable sum(1 for _, solvable in results if solvable) total len(combos) print(f可解比例: {solvable/total:.2%})6. 项目扩展与深度应用基础功能实现后这个项目还有很多可以挖掘的方向让它从一个算法练习变成一个有趣的分析工具甚至小游戏。6.1 扩展一开发一个交互式24点游戏使用input或简单的图形库如tkinter,pygame可以做一个用户与电脑对抗的游戏。模式一电脑出题用户解答。程序随机生成有解的四张牌用户输入算式程序用eval验证需做安全过滤并判断是否正确。模式二用户出题电脑解答。用户输入四个数字程序快速计算并给出一个解或所有解。增加计时、计分、难度分级例如限制只能用加减乘除或允许乘方、开方。6.2 扩展二表达式树可视化使用graphviz或matplotlib库将找到的解法对应的表达式树画出来。这能帮助你更直观地理解运算的结合顺序。你需要修改算法让它返回树形结构可以用嵌套列表或自定义节点类表示然后进行可视化。class Node: def __init__(self, value, leftNone, rightNone, opNone): self.value value # 叶子节点存数字内部节点存运算符 self.left left self.right right self.op op # 运算符叶子节点为None def is_leaf(self): return self.left is None and self.right is None在递归过程中不再构建表达式字符串而是构建Node对象。最终得到的根节点就是整个表达式树。6.3 扩展三概率统计与“最难”的24点组合利用我们之前写的analyze_all_combinations函数我们不仅可以得到有解比例还可以找出所有无解的组合例如[1, 1, 1, 1],[1, 1, 1, 2]等。这些可以作为游戏中的“死局”。定义并找出“最难”的组合什么样的组合最难可以是有解但解法数量最少的组合或者是人类直觉最难想到解的组合例如[3, 3, 8, 8]的解法8 / (3 - 8/3)需要用到分数。通过收集所有解并计算每个组合的解的个数就能进行排序分析。分析运算符分布在所有成功解中加、减、乘、除出现的频率各是多少括号的平均深度是多少这能揭示这个游戏的数学特性。6.4 扩展四支持更多运算符和规则标准的24点只用了四则运算。你可以扩展它增加乘方和开方sqrt**例如[2, 5, 7, 13]可以用(5*137)/2但也许有更巧妙的用到乘方的解。注意开方可能产生无理数对精度处理和解的判断带来挑战。允许数字拼接例如[1, 2, 3, 4]可以拼接成123419或者3和4拼接成34。这大大增加了可能性几乎使所有组合都有解。允许使用常数如圆周率π、自然常数e。每增加一条规则算法的搜索空间就会指数级扩大对程序的设计和性能都是新的挑战。7. 项目总结与个人体会走完整个项目从最开始的暴力穷举思路到引入递归回溯和记忆化优化再到实现表达式输出和进行大规模统计分析最后思考各种扩展可能这不仅仅是一个“算24点”的程序而是一个完整的计算思维训练过程。我个人最深的体会是清晰的抽象是成功的一半。最初我被“所有可能的表达式”这个模糊概念困扰。一旦将其分解为“数字排列”、“运算符填充”、“表达式树结构”三个相对独立的子问题思路就豁然开朗。递归回溯算法之所以自然正是因为它对应了“任选两数合并问题规模减小”这一最本质的解题动作。另一个收获是对浮点数精度的深刻认识。在数学上是整数的运算在计算机里用浮点数表示时可能产生微小误差。math.isclose是这个项目也是所有涉及浮点数比较的科学计算项目的必备工具。在性能优化上避免重复计算是永恒的真理。记忆化搜索缓存在这个问题上的效果立竿见影。这也让我联想到动态规划的核心思想——以空间换时间存储子问题的解。最后这个项目的延展性令人惊喜。它像一棵树从主干可以生出许多枝丫做成游戏、进行数学分析、可视化、支持更复杂的规则。这提醒我一个好的练手项目不应该止步于功能实现而应该主动去思考“还能做什么”这往往是学习和创造力的源泉。如果你跟着实现了一遍我建议你尝试这个挑战修改算法让它能找出所有本质不同的解即去除因加法和乘法交换律、结合律而产生的重复解。这需要你定义什么是“本质相同”并可能涉及到表达式的规范化或语法树的同构判断是一个更有深度的算法问题。