You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为基于Y组合子的F#找零计算函数实现缓存优化

解决找零递归函数的缓存与性能问题

先给你梳理下核心问题:你原来的递归函数带了acc累加器,不仅逻辑上有偏差(会把有序的硬币组合当成不同方式,比如1+2和2+1会被重复计数,不符合欧拉计划第31题的要求),还直接导致缓存失效——每次递归的acc都不一样,缓存key全是独一无二的,自然起不到复用计算的作用。下面一步步帮你解决:

步骤1:修正递归逻辑,去掉多余的acc

先写出正确的无累加器递归逻辑:对于当前硬币,要么选它(金额减少,硬币列表不变,因为可以重复选),要么不选它(硬币列表去掉当前硬币,金额不变),两种情况的方式数相加就是最终结果:

let coins = [200; 100; 50; 20; 10; 5; 2; 1]

// 正确的递归逻辑:返回给定硬币子集和金额的找零方式数
let rec countWays coins amount =
    if amount < 0 then 0L
    elif amount = 0 then 1L  // 找到一种有效组合
    else
        match coins with
        | [] -> 0L  // 没硬币可选,无法凑出金额
        | c::cs -> countWays coins (amount - c) + countWays cs amount

步骤2:调整参数,让缓存可以命中

直接用硬币列表当缓存key效率不高(F#列表是引用类型),我们可以利用硬币列表固定有序的特点,用起始索引代表当前可用的硬币子集(比如索引i代表从coins.[i]开始的子列表),这样缓存key就变成(int * int)(索引+金额),非常适合作为字典的key。

同时改成适配Y组合子的形式(需要传入自身作为参数):

// 用索引i代替硬币列表,i表示当前可用的起始硬币索引
let countWaysSelf self i amount =
    if amount < 0 then 0L
    elif amount = 0 then 1L
    elif i >= coins.Length then 0L
    else
        let currentCoin = coins.[i]
        // 选当前硬币:金额减少,索引不变(可以重复选)
        self i (amount - currentCoin) +
        // 不选当前硬币:索引+1,金额不变
        self (i + 1) amount

步骤3:结合Y组合子与缓存实现高效计算

现在实现带缓存的memoization函数,再用Y组合子绑定递归,重复的(索引, 金额)计算会直接从缓存读取:

open System.Collections.Generic

// 通用的memoization函数:接收字典和函数,返回带缓存的函数
let memoize (dict: Dictionary<_, _>) f =
    fun args ->
        match dict.TryGetValue(args) with
        | true, result -> result
        | false, _ ->
            let result = f args
            dict.Add(args, result)
            result

// Y组合子实现递归
let rec Y f = f (Y f)

// 最终的找零方式数计算函数
let numberOfWaysToChange amount =
    let cache = Dictionary<(int * int), int64>()
    // 把参数打包成元组,方便缓存key处理
    let wrappedSelf self (i, amt) = countWaysSelf self i amt
    // 用Y组合子绑定递归,同时套上缓存
    Y (fun self -> memoize cache (wrappedSelf self)) (0, amount)

关于你想要的Dictionary<int, int64>

这个需求其实不可行,因为同一个金额,在不同的硬币子集下的找零方式数完全不同:比如金额5,用[5;2;1]的方式数是4(5、2+2+1、2+1+1+1、1*5),但用[2;1]的方式数是3,所以必须把硬币子集的状态(这里用索引)加入缓存key,才能保证缓存的正确性。

额外优化:解决栈溢出问题

如果要处理超大金额,上面的递归还是可能栈溢出,这时候可以改成尾递归或者续传风格(CPS),比如尾递归版本:

// 尾递归版本,用累加器acc保存中间结果
let countWaysTailRec amount =
    let rec loop i amt acc =
        if amt < 0 then acc
        elif amt = 0 then acc + 1L
        elif i >= coins.Length then acc
        else
            // 选当前硬币:继续用当前索引,金额减少
            loop i (amt - coins.[i]) acc +
            // 不选当前硬币:索引+1,金额不变
            loop (i + 1) amt acc
    loop 0 amount 0L

你可以把之前的memoization逻辑套到这个尾递归版本上,进一步提升大金额的处理效率。

内容的提问来源于stack exchange,提问作者primfaktor

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 08:24:42