Optimization 如何迭代一组总和约为0的数字的所有子集

Optimization 如何迭代一组总和约为0的数字的所有子集,optimization,math,f#,functional-programming,Optimization,Math,F#,Functional Programming,现在,我已经有将近20年没有应用于函数式编程了,那时我们只写阶乘和fib,所以我真的呼吁社区帮我找到解决方案 我的问题是: “给定一组交易对象,我想找出净公差为零+/-一定公差的所有交易组合。” 我的第一个十年计划是: let NettedOutTrades trades tolerance = ... 让我们假设我的起点是先前构造的元组数组(trade,value)。我想要的是一个数组(或者列表,随便什么)的交易数组。因此: let result = NettedOutTrades [| (

现在,我已经有将近20年没有应用于函数式编程了,那时我们只写阶乘和fib,所以我真的呼吁社区帮我找到解决方案

我的问题是:

“给定一组交易对象,我想找出净公差为零+/-一定公差的所有交易组合。”

我的第一个十年计划是:

let NettedOutTrades trades tolerance = ...
让我们假设我的起点是先前构造的元组数组(trade,value)。我想要的是一个数组(或者列表,随便什么)的交易数组。因此:

let result = NettedOutTrades [| (t1, -10); (t2, 6); (t3, 6); (t4; 5) |] 1
将导致:

   [| 
     [| t1; t2; t4 |]
     [| t1; t3; t4 |]
   |]
我认为这可以通过一个尾部递归构造来实现,使用两个累加器——一个用于结果,一个用于交易值之和。但如何把这一切放在一起


我相信我可以用c#完成一些程序性的东西,但它感觉不太适合这项工作——我确信使用函数范式会有一个优雅、简洁、高效的解决方案……我只是目前没有足够的实践来识别它

这里有一种编写所需函数的函数方法。它是一个简单的功能实现,没有任何使用列表的巧妙优化。它不是尾部递归的,因为它需要为每次交易递归调用自己两次:

let nettedOutTrades trades tolerance =
  // Recursively process 'remaining' trades. Currently accumulated trades are
  // stored in 'current' and the sum of their prices is 'sum'. The accumulator
  // 'result' stores all lists of trades that add up to 0 (+/- tolerance)
  let rec loop remaining sum current result =
    match remaining with 
    // Finished iterating over all trades & the current list of trades
    // matches the condition and is non-empty - add it to results
    | [] when sum >= -tolerance && sum <= tolerance &&
              current <> [] -> current::result
    | [] -> result // Finished, but didn't match condition
    | (t, p)::trades -> 
      // Process remaining trades recursively using two options:
      // 1) If we add the trade to current trades
      let result = loop trades (sum + p) (t::current) result
      // 2) If we don't add the trade and skip it
      loop trades sum current result
  loop trades 0 [] [] 
让nettedOutTrades交易容差=
//递归处理“剩余”交易。目前累计交易为
//存储在“当前”中,其价格总和为“总和”。蓄能器
//“结果”存储总计为0(+/-容差)的所有交易列表
让rec循环剩余和当前结果=
与剩余的匹配
//完成了对所有交易和当前交易列表的迭代
//匹配条件且为非空-将其添加到结果
|[]当总和>=-公差和总和电流::结果
|[]->结果//已完成,但不符合条件
|(t,p)::交易->
//使用两个选项递归处理剩余交易:
//1)如果我们将交易添加到当前交易中
让结果=循环交易(总和+p)(t::当前)结果
//2)如果我们不添加交易并跳过它
循环交易和当前结果
循环交易0[][]

该函数递归地处理所有组合,因此它不是特别有效(但可能没有更好的方法)。只有在第二次调用
循环时,它才是尾部递归的,但要使它完全尾部递归,需要继续,这会使示例更复杂。

因为@Tomas已经给出了一个直接的解决方案,我想我会提出一个解决方案,强调高阶函数的组合是函数编程中常用的一种强大技术;该问题可分解为三个离散步骤:

  • 生成一组元素的所有组合。这是问题中最困难和可重用的部分。因此,我们将问题的这一部分隔离到一个独立函数中,该函数返回给定通用元素列表的组合序列
  • 给定(交易、价值)列表,筛选出价值总和不在给定公差范围内的所有组合
  • 将每个组合从(交易、价值)列表映射到交易列表
  • 我提升了@Tomas计算所有(除了空的)组合的底层算法 但使用递归序列表达式,而不是带有累加器的递归函数(我发现这更容易读写)


    这很有趣。我发现有两种延续:构建器延续和处理延续

    无论如何;这与子集和问题非常相似,它是NP完全问题。因此,可能没有比列举所有可能性并选择符合标准的可能性更快的算法了

    不过,您实际上不需要根据生成的组合构建数据结构。如果只调用带有每个结果的函数更有效

    /// Takes some input and a function to receive all the combinations
    /// of the input.
    ///   input:    List of any anything
    ///   iterator: Function to receive the result.
    let iterCombinations input iterator =
      /// Inner recursive function that does all the work.
      ///   remaining: The remainder of the input that needs to be processed
      ///   builder:   A continuation that is responsible for building the
      ///              result list, and passing it to the result function.
      ///   cont:      A normal continuation, just used to make the loop tail
      ///              recursive.
      let rec loop remaining builder cont =
        match remaining with
        | [] ->
            // No more items; Build the final value, and continue with
            // queued up work.
            builder []
            cont()
        | (x::xs) ->
            // Recursively build the list with (and without) the current item.
            loop xs builder <| fun () ->
              loop xs (fun ys -> x::ys |> builder) cont
      // Start the loop.
      loop input iterator id
    
    /// Searches for sub-lists which has a sum close to zero.
    let nettedOutTrades tolerance items =
      // mutable accumulator list
      let result = ref []
      iterCombinations items <| function
        | [] -> () // ignore the empty list, which is always there
        | comb ->
            // Check the sum, and add the list to the result if
            // it is ok.
            let sum = comb |> List.sumBy snd
            if abs sum <= tolerance then
              result := (List.map fst comb, sum) :: !result
      !result
    

    使用生成器延续而不是累加器的原因是,您获得的结果与传入的结果的顺序相同,而无需反转。

    可能需要编辑您的示例-应该是(t1,-11),这样他们就可以算出每个交易的问题相当于子集和问题()这是NP完全。@BrokenGlass:I asume Brett将1定义为原始问题中提到的公差间隙(“…该网络为零+/-一些公差”)。没错,froeschli,这个例子是为了说明公差的使用……但公平地说,这可能是问题中最微不足道的部分,为了清楚起见,可以省略掉。+1@Tomas,你真的很快就完成了这个例子,然后回来并添加了精彩的评论。一个小建议:翻转
    nettedOutTrades
    trades
    tolerance
    参数,这样您就可以在交易中通过公差和管道进行交易。+1@Tomas,感谢您提供的伟大解决方案。我接受了Stephen的回答,因为它分解了各个组成部分,我可以看到,在我的需求范围(不可避免地)逐渐扩大的将来,这将是多么有用。
    [("a", 2); ("b", -1); ("c", -2); ("d", 1)] |> nettedOutTrades 1
    
    /// Takes some input and a function to receive all the combinations
    /// of the input.
    ///   input:    List of any anything
    ///   iterator: Function to receive the result.
    let iterCombinations input iterator =
      /// Inner recursive function that does all the work.
      ///   remaining: The remainder of the input that needs to be processed
      ///   builder:   A continuation that is responsible for building the
      ///              result list, and passing it to the result function.
      ///   cont:      A normal continuation, just used to make the loop tail
      ///              recursive.
      let rec loop remaining builder cont =
        match remaining with
        | [] ->
            // No more items; Build the final value, and continue with
            // queued up work.
            builder []
            cont()
        | (x::xs) ->
            // Recursively build the list with (and without) the current item.
            loop xs builder <| fun () ->
              loop xs (fun ys -> x::ys |> builder) cont
      // Start the loop.
      loop input iterator id
    
    /// Searches for sub-lists which has a sum close to zero.
    let nettedOutTrades tolerance items =
      // mutable accumulator list
      let result = ref []
      iterCombinations items <| function
        | [] -> () // ignore the empty list, which is always there
        | comb ->
            // Check the sum, and add the list to the result if
            // it is ok.
            let sum = comb |> List.sumBy snd
            if abs sum <= tolerance then
              result := (List.map fst comb, sum) :: !result
      !result
    
    > [("a",-1); ("b",2); ("c",5); ("d",-3)]
    - |> nettedOutTrades 1
    - |> printfn "%A"
    
    [(["a"; "b"], 1); (["a"; "c"; "d"], 1); (["a"], -1); (["b"; "d"], -1)]