简体   繁体   English

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

[英]How to iterate over all subsets of a set of numbers that sum to around 0

Now, I haven't applied myself to functional programming for, oh, nearly 20 years, when we didn't get much further than writing factorials and fibs, so I'm really appealing to the community for some help in finding a solution. 现在,我已经没有将自己应用于函数式编程了,近20年,当我们没有比写因子和文本更进一步时,所以我真的很想吸引社区寻求解决方案。

My problem is this: 我的问题是:

"Given a group of trade objects, I want to find all the combinations of trades that net to zero +/- some tolerance." “鉴于一组交易对象,我想找到所有交易的组合,净值为零+/-一些容差。”

My starter for ten is: 我十岁的首发是:

let NettedOutTrades trades tolerance = ...

Let's assume my starting point is a previously constructed array of tuples (trade, value). 让我们假设我的起点是先前构造的元组数组(交易,价值)。 What I want back is an array (or list, whatever) of arrays of trades that net out. 我想要的是一个数组(或列表,无论如何)的交易数组。 Thus: 从而:

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

would result in: 会导致:

   [| 
     [| t1; t2; t4 |]
     [| t1; t3; t4 |]
   |]

I'm thinking that this could be achieved with a tail recursive construct, using two accumulators - one for the results and one for the sum of trade values. 我认为这可以通过尾递归构造实现,使用两个累加器 - 一个用于结果,一个用于交易值的总和。 But how to put it all together...? 但是如何把它们放在一起......?

I'm sure I could knock out something procedural using c#, but it just doesn't feel like the right tool for the job - I'm convinced there's going to be an elegant, concise, efficient solution using the functional paradigm...I'm just not well practiced enough to identify it at present! 我确信我可以使用c#删除一些程序性的东西,但它只是感觉不适合这项工作的工具 - 我相信使用功能范例会有一个优雅,简洁,高效的解决方案......我目前还没有很好地练习识别它!

Here is one functional way to write the function you want. 这是编写所需函数的一种功能方法。 It is a straightforward functional implementation without any clever optimizations that uses lists. 这是一个简单的功能实现,没有使用列表的任何聪明的优化。 It isn't tail-recursive, because it needs to call itself recursively two times for each trade: 它不是尾递归的,因为它需要为每个交易递归调用两次:

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 [] [] 

The function recursively processes all combinations, so it is not particularly efficient (but there probably isn't any better way). 该函数递归地处理所有组合,因此它不是特别有效(但可能没有更好的方法)。 It is tail-recursive only in the second call to loop , but to make it fully tail-recursive, you'd need continuations , which would make the example a bit more complex. 它仅在第二次loop调用中是尾递归的,但为了使其完全是尾递归,你需要连续性 ,这会使示例更复杂一些。

Since @Tomas already gave a direct solution, I thought I'd present a solution which highlights composition with higher-order functions as a powerful technique commonly used in functional programming; 由于@Tomas已经提供了一个直接的解决方案,我想我会提出一个解决方案,它突出了具有高阶函数的组合作为函数式编程中常用的强大技术; this problem can be decomposed into three discrete steps: 这个问题可以分解为三个不连续的步骤:

  1. Generate all combinations of a set of elements. 生成一组元素的所有组合。 This is the most difficult and reusable piece of the problem. 这是问题中最困难和可重复使用的部分。 Therefore, we isolate this part of the problem into a stand-alone function which returns a sequence of combinations given a generic list of elements. 因此,我们将问题的这一部分隔离成一个独立的函数,该函数返回给定一个通用元素列表的一系列组合。
  2. Given list of (trade,value), filter out all combinations with value sums not within a given tolerance. 给定(交易,价值)清单,过滤掉不在给定容差范围内的价值总和的所有组合。
  3. Map each combination from a list of (trade,value) to a list of trade. 将每个组合从(贸易,价值)列表映射到交易清单。

I lifted @Tomas's underlying algorithm for calculating all (expect the empty) combinations of a set but use a recursive sequence expression instead of a recursive function with an accumulator (I find this slightly easier to read and write). 我解除了@ Tomas用于计算集合的所有(期望空)组合的基础算法,但是使用递归序列表达式而不是使用累加器的递归函数(我发现这稍微更容易读取和写入)。

let combinations input =
    let rec loop remaining current = seq {
        match remaining with 
        | [] -> ()
        | hd::tail -> 
            yield  hd::current
            yield! loop tail (hd::current)
            yield! loop tail current
    }
    loop input []

let nettedOutTrades tolerance trades =
    combinations trades
    |> Seq.filter
        (fun tradeCombo -> 
            tradeCombo |> List.sumBy snd |> abs <= tolerance)
    |> Seq.map (List.map fst)

I swapped the order of trades and tolerance in your proposed function signature, since it makes it easier to curry by tolerance and pipe in (trade,value) lists which is the typical style used in the F# community and generally encouraged by the F# library. 我在你提出的函数签名中交换了tradestolerance的顺序,因为它更容易通过容差和管道(交易,价值)列表进行咖喱,这是F#社区中使用的典型样式,并且通常由F#库鼓励。 eg: 例如:

[("a", 2); ("b", -1); ("c", -2); ("d", 1)] |> nettedOutTrades 1

This was interesting. 这很有趣。 I discovered there are two kinds of continuations: A builder continuation, and a processing continuation. 我发现有两种延续:构建器延续和处理延续。

Anyway; 无论如何; This is very similar to the Subset-sum problem, which is NP-complete. 这与子集和问题非常相似,它是NP完全的。 Thus there is probably no faster algorithm than enumerating all possibilities, and choosing those that match the criterion. 因此,可能没有比枚举所有可能性更快的算法,并选择那些符合标准的算法。

Though, you don't actually need to build a data-structure out of the combinations that are generated. 但是,实际上并不需要从生成的组合中构建数据结构。 If it more efficient to just call a function with each result. 如果用每个结果调用一个函数更有效。

/// 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

For example: 例如:

> [("a",-1); ("b",2); ("c",5); ("d",-3)]
- |> nettedOutTrades 1
- |> printfn "%A"

[(["a"; "b"], 1); (["a"; "c"; "d"], 1); (["a"], -1); (["b"; "d"], -1)]

The reason for using a builder continuation instead of an accumulator is that you get the result in same order as was passed in, without having to reverse it. 使用构建器连续而不是累加器的原因是,您获得的结果与传入的顺序相同,而不必反转它。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM