简体   繁体   中英

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.

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!

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.

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; 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).

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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