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

时间:2010-12-31 15:55:13

标签: optimization math f# functional-programming

现在,我已经没有将自己应用于函数式编程了,近20年,当我们没有比编写因子和文本更进一步,所以我真的很想吸引社区寻求帮助解决方案。

我的问题是:

“鉴于一组交易对象,我想找到所有交易的组合,净值为零+/-一些容差。”

我的十岁开头是:

let NettedOutTrades trades tolerance = ...

让我们假设我的起点是先前构造的元组数组(交易,价值)。我想要的是一个数组(或列表,无论如何)的交易数组。因此:

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

会导致:

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

我认为这可以通过尾递归构造实现,使用两个累加器 - 一个用于结果,一个用于交易值的总和。但是如何把它们放在一起......?

我确信我可以使用c#删除一些程序性的东西,但它只是感觉不适合这项工作的工具 - 我相信使用功能范例会有一个优雅,简洁,高效的解决方案......我目前还没有足够的练习来识别它!

3 个答案:

答案 0 :(得分:8)

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

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

该函数以递归方式处理所有组合,因此效率不高(但可能没有更好的方法)。它仅在第二次调用loop时是尾递归的,但为了使它完全是尾递归的,你需要 continuations ,这会使这个例子更加复杂。 / p>

答案 1 :(得分:4)

由于@Tomas已经提供了一个直接的解决方案,我想我会提出一个解决方案,它突出了具有高阶函数的组合作为函数式编程中常用的强大技术;这个问题可以分解为三个不连续的步骤:

  1. 生成一组元素的所有组合。这是问题中最困难和可重复使用的部分。因此,我们将问题的这一部分隔离成一个独立的函数,该函数返回给定泛型元素列表的一系列组合。
  2. 给定(交易,价值)清单,过滤掉所有价值总和不在给定容差范围内的组合。
  3. 将每个组合从(贸易,价值)列表映射到交易清单。
  4. 我解除了@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)
    

    我在您建议的功能签名中交换了tradestolerance的顺序,因为它更容易通过容差和管道(交易,价值)列表进行咖喱,这是典型的风格。 F#社区,通常受到F#库的鼓励。 e.g:

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

答案 2 :(得分:1)

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

反正;这与子集和问题非常相似,它是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

例如:

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

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

使用构建器连续而不是累加器的原因是,您获得的结果与传入的顺序相同,而不必反转它。