
本文详解如何修正递归组合计数函数,使其支持同一单词多次复用(如 "p" + "ur" + "p" + "le" 构成 "purple"),并给出可运行、可调试的 Python 实现及关键设计原理。
本文详解如何修正递归组合计数函数,使其支持同一单词多次复用(如 `"p" + "ur" + "p" + "le"` 构成 `"purple"`),并给出可运行、可调试的 python 实现及关键设计原理。
在原始实现中,count_construct 函数采用“索引推进式”递归(index 参数控制遍历位置),导致每个单词最多被选用一次——这本质上是子集和问题的变体,而非组合总和问题。但本题目标是:从 word_bank 中有放回地选取单词(即允许重复使用任意单词),拼接成 target 字符串,并统计所有可能的拼接方案数。因此,递归状态不应绑定于 word_bank 的下标,而应聚焦于当前已构造的字符串前缀与剩余目标。
✅ 正确思路:基于剩余目标的深度优先递归
核心思想是:对当前待匹配的 target 前缀,遍历整个 word_bank,对每一个能匹配开头的单词 w(即 target.startswith(w)),递归求解剩余部分 target[len(w):] 的方案数。这种设计天然支持重复使用——每次递归都重新扫描全部单词,无索引限制。
以下是优化后的专业实现(含调试输出与边界防护):
def count_construct(target, word_bank):
# 缓存已计算的子问题结果,避免重复递归(可选但强烈推荐)
memo = {}
def helper(remaining):
# 基础情况:已完全匹配
if remaining == "":
return 1
# 记忆化剪枝
if remaining in memo:
return memo[remaining]
total = 0
# 尝试每个单词:只要它能匹配 remaining 开头,就递归处理剩余部分
for w in word_bank:
if len(w) <= len(remaining) and remaining.startswith(w):
total += helper(remaining[len(w):])
memo[remaining] = total
return total
return helper(target)✅ 验证示例:
target_example = "purple" word_bank_example = ["purp", "p", "ur", "le", "purpl"] print(count_construct(target_example, word_bank_example)) # 输出:2
结果正确:["purp","le"] 和 ["p","ur","p","le"] 两种方案。
? 为什么原代码只返回 1?
原函数中 index 参数强制线性遍历,helper(index, current + word_bank[index]) 仅允许在当前位置复用当前单词,但无法在后续步骤中“回头”再次选用更靠前的单词(如第一个 "p" 用过后,index 已推进,无法再取 "p")。而新设计中,每次 for w in word_bank: 都完整重扫,彻底解除复用限制。
⚠️ 注意事项与最佳实践
- 字符串切片安全:务必检查 len(w)
- 记忆化(Memoization)至关重要:本问题存在大量重叠子问题(如 "ple" 可能被多次计算),加入 memo 可将时间复杂度从指数级降至多项式级(O(n·m),其中 n 为 target 长度,m 为 word_bank 大小)。
- 避免副作用与全局状态:对比答案中使用 nonlocal count 和列表 current 的调试版,生产代码应优先采用纯函数式递归(返回值累加),更易测试、调试和并发安全。
- 输入校验(进阶):可添加 if not word_bank or not target: 短路返回;对空字符串单词做特殊处理(通常应过滤掉)。
✅ 总结
要让递归函数支持元素重复使用,关键在于解耦递归状态与输入容器的遍历索引,转而以“问题剩余规模”(如未匹配的字符串)为状态变量,并在每层递归中全量、无偏地尝试所有可行选项。配合记忆化,该模式可高效解决经典的 canConstruct / countConstruct / allConstruct 系列动态规划问题。掌握此范式,即掌握了递归建模中“有放回选择”的核心设计哲学。










